Repository: linkerd/linkerd2
Pull request #5942 (2021-03-23 17:23:40)

Get rid of `CheckDeployments()` in integration tests
`CheckDeployments()` verified that some deployment had the appropriate
number of replicas in the Ready state. `CheckPods()` does the same, plus
checking if there were restarts (a single restart returns
`RestartCountError` which only triggers a warning on the calling side,
more restarts trigger a regular error). We were always calling the
former followed by a call to the latter which is superfluous, so we're
getting rid of the latter.

Also, the `testutil.DeploySpec` struct had a `Containers` field for
checking the name of containers, but that wasn't always checked and
didn't really represent a potential error that wouldn't be clearly
manifested otherwise (like in golden files), so that was simplified as
well.



Patch:
diff --git a/cli/cmd/check.go b/cli/cmd/check.go
index ef2e10ad3..bc88921be 100644
--- a/cli/cmd/check.go
+++ b/cli/cmd/check.go
@@ -288,7 +288,7 @@ func runExtensionChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, opts
 					Category:    healthcheck.CategoryID(extensionCmd),
 					Description: fmt.Sprintf("Linkerd extension command %s exists", extensionCmd),
 					Err:         err,
-					HintAnchor:  "extensions",
+					HintURL:     healthcheck.DefaultHintBaseURL + "extensions",
 					Warning:     true,
 				},
 			}
@@ -316,7 +316,7 @@ func runExtensionChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, opts
 					Category:    healthcheck.CategoryID(extensionCmd),
 					Description: fmt.Sprintf("Running: %s", command),
 					Err:         err,
-					HintAnchor:  "extensions",
+					HintURL:     healthcheck.DefaultHintBaseURL + "extensions",
 				})
 				success = false
 			} else {
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 4dd55ea8a..0c0f6f435 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -160,10 +160,10 @@ const (
 	keyKeyName                    = "tls.key"
 )
 
-// HintBaseURL is the base URL on the linkerd.io website that all check hints
+// DefaultHintBaseURL is the base URL on the linkerd.io website that all check hints
 // point to. Each check adds its own `hintAnchor` to specify a location on the
 // page.
-const HintBaseURL = "https://linkerd.io/checks/#"
+const DefaultHintBaseURL = "https://linkerd.io/checks/#"
 
 // AllowedClockSkew sets the allowed skew in clock synchronization
 // between the system running inject command and the node(s), being
@@ -355,7 +355,7 @@ func (c *Checker) WithCheck(check func(context.Context) error) *Checker {
 type CheckResult struct {
 	Category    CategoryID
 	Description string
-	HintAnchor  string
+	HintURL     string
 	Retry       bool
 	Warning     bool
 	Err         error
@@ -369,17 +369,28 @@ type Category struct {
 	ID       CategoryID
 	checkers []Checker
 	enabled  bool
+	// hintBaseURL provides a base URL with more information
+	// about the check
+	hintBaseURL string
 }
 
 // NewCategory returns an instance of Category with the specified data
+// and the DefaultHintBaseURL
 func NewCategory(id CategoryID, checkers []Checker, enabled bool) *Category {
 	return &Category{
-		ID:       id,
-		checkers: checkers,
-		enabled:  enabled,
+		ID:          id,
+		checkers:    checkers,
+		enabled:     enabled,
+		hintBaseURL: DefaultHintBaseURL,
 	}
 }
 
+// WithHintBaseURL returns a Category with the provided hintBaseURL
+func (c *Category) WithHintBaseURL(hintBaseURL string) *Category {
+	c.hintBaseURL = hintBaseURL
+	return c
+}
+
 // Options specifies configuration for a HealthChecker.
 type Options struct {
 	ControlPlaneNamespace string
@@ -1483,7 +1494,7 @@ func (hc *HealthChecker) RunChecks(observer CheckObserver) bool {
 			for _, checker := range c.checkers {
 				checker := checker // pin
 				if checker.check != nil {
-					if !hc.runCheck(c.ID, &checker, observer) {
+					if !hc.runCheck(c, &checker, observer) {
 						if !checker.warning {
 							success = false
 						}
@@ -1504,7 +1515,7 @@ func (hc *HealthChecker) LinkerdConfig() *l5dcharts.Values {
 	return hc.linkerdConfig
 }
 
-func (hc *HealthChecker) runCheck(categoryID CategoryID, c *Checker, observer CheckObserver) bool {
+func (hc *HealthChecker) runCheck(category Category, c *Checker, observer CheckObserver) bool {
 	for {
 		ctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)
 		defer cancel()
@@ -1515,15 +1526,15 @@ func (hc *HealthChecker) runCheck(categoryID CategoryID, c *Checker, observer Ch
 		}
 
 		checkResult := &CheckResult{
-			Category:    categoryID,
+			Category:    category.ID,
 			Description: c.description,
-			HintAnchor:  c.hintAnchor,
 			Warning:     c.warning,
+			HintURL:     fmt.Sprintf("%s%s", category.hintBaseURL, c.hintAnchor),
 		}
 		if vs, ok := err.(*VerboseSuccess); ok {
 			checkResult.Description = fmt.Sprintf("%s\n%s", checkResult.Description, vs.Message)
 		} else if err != nil {
-			checkResult.Err = &CategoryError{categoryID, err}
+			checkResult.Err = &CategoryError{category.ID, err}
 		}
 
 		if checkResult.Err != nil && time.Now().Before(c.retryDeadline) {
diff --git a/pkg/healthcheck/healthcheck_output.go b/pkg/healthcheck/healthcheck_output.go
index 0311eec6a..050a94a14 100644
--- a/pkg/healthcheck/healthcheck_output.go
+++ b/pkg/healthcheck/healthcheck_output.go
@@ -93,8 +93,8 @@ func runChecksTable(wout io.Writer, hc Runner) bool {
 		fmt.Fprintf(wout, "%s %s\n", status, result.Description)
 		if result.Err != nil {
 			fmt.Fprintf(wout, "    %s\n", result.Err)
-			if result.HintAnchor != "" {
-				fmt.Fprintf(wout, "    see %s%s for hints\n", HintBaseURL, result.HintAnchor)
+			if result.HintURL != "" {
+				fmt.Fprintf(wout, "    see %s for hints\n", result.HintURL)
 			}
 		}
 	}
@@ -170,8 +170,8 @@ func runChecksJSON(wout io.Writer, werr io.Writer, hc Runner) bool {
 			if result.Err != nil {
 				currentCheck.Error = result.Err.Error()
 
-				if result.HintAnchor != "" {
-					currentCheck.Hint = fmt.Sprintf("%s%s", HintBaseURL, result.HintAnchor)
+				if result.HintURL != "" {
+					currentCheck.Hint = result.HintURL
 				}
 			}
 			currentCategory.Checks = append(currentCategory.Checks, currentCheck)
@@ -215,7 +215,7 @@ func ParseJSONCheckOutput(data []byte) (CheckResults, error) {
 				Category:    CategoryID(category.Name),
 				Description: check.Description,
 				Err:         err,
-				HintAnchor:  check.Hint,
+				HintURL:     check.Hint,
 				Warning:     check.Result == checkWarn,
 			})
 		}
diff --git a/web/srv/api_handlers.go b/web/srv/api_handlers.go
index 18b2a13f4..3671fa46d 100644
--- a/web/srv/api_handlers.go
+++ b/web/srv/api_handlers.go
@@ -368,7 +368,7 @@ func (h *handler) handleAPICheck(w http.ResponseWriter, req *http.Request, p htt
 				success = false
 			}
 			errMsg = result.Err.Error()
-			hintURL = fmt.Sprintf("%s%s", healthcheck.HintBaseURL, result.HintAnchor)
+			hintURL = result.HintURL
 		}
 		results[result.Category] = append(results[result.Category], &CheckResult{
 			CheckResult: result,


Test patch:
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index a3ecf8b52..89929458a 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -43,6 +43,18 @@ func (o *observer) resultFn(result *CheckResult) {
 	o.results = append(o.results, res)
 }
 
+func (o *observer) resultWithHintFn(result *CheckResult) {
+	res := fmt.Sprintf("%s %s", result.Category, result.Description)
+	if result.Err != nil {
+		res += fmt.Sprintf(": %s", result.Err)
+	}
+
+	if result.HintURL != "" {
+		res += fmt.Sprintf(": %s", result.HintURL)
+	}
+	o.results = append(o.results, res)
+}
+
 func (hc *HealthChecker) addCheckAsCategory(
 	testCategoryID CategoryID,
 	categoryID CategoryID,
@@ -156,6 +168,20 @@ func TestHealthChecker(t *testing.T) {
 		enabled: true,
 	}
 
+	troubleshootingCheck := Category{
+		ID: "cat9",
+		checkers: []Checker{
+			{
+				description: "failCheck",
+				hintAnchor:  "cat9",
+				check: func(context.Context) error {
+					return fmt.Errorf("fatal")
+				},
+			},
+		},
+		enabled: true,
+	}
+
 	t.Run("Notifies observer of all results", func(t *testing.T) {
 		hc := NewHealthChecker(
 			[]CategoryID{},
@@ -211,6 +237,25 @@ func TestHealthChecker(t *testing.T) {
 		}
 	})
 
+	t.Run("Check for troubleshooting URL", func(t *testing.T) {
+		hc := NewHealthChecker(
+			[]CategoryID{},
+			&Options{},
+		)
+		troubleshootingCheck.WithHintBaseURL("www.extension.com/troubleshooting/#")
+		hc.AppendCategories(troubleshootingCheck)
+		expectedResults := []string{
+			"cat9 failCheck: fatal: www.extension.com/troubleshooting/#cat9",
+		}
+
+		obs := newObserver()
+		hc.RunChecks(obs.resultWithHintFn)
+
+		if !reflect.DeepEqual(obs.results, expectedResults) {
+			t.Fatalf("Expected results %v, but got %v", expectedResults, obs.results)
+		}
+	})
+
 	t.Run("Does not run remaining check if fatal check fails", func(t *testing.T) {
 		hc := NewHealthChecker(
 			[]CategoryID{},
diff --git a/test/integration/edges/edges_test.go b/test/integration/edges/edges_test.go
index 132187dc6..c184de3d6 100644
--- a/test/integration/edges/edges_test.go
+++ b/test/integration/edges/edges_test.go
@@ -87,10 +87,6 @@ func TestDirectEdges(t *testing.T) {
 			}
 		}
 
-		if err := TestHelper.CheckDeployment(ctx, testNamespace, "terminus", 1); err != nil {
-			testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", "terminus", err)
-		}
-
 		// get terminus pod ip
 
 		ip, err := TestHelper.Kubectl("", "-n", testNamespace, "get", "pod", "-ojsonpath=\"{.items[*].status.podIP}\"")
@@ -129,10 +125,6 @@ func TestDirectEdges(t *testing.T) {
 			}
 		}
 
-		if err := TestHelper.CheckDeployment(ctx, testNamespace, "slow-cooker", 1); err != nil {
-			testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "error validating deployment [%s]:\n%s", "terminus", err)
-		}
-
 		// check edges
 		timeout := 50 * time.Second
 		testDataPath := "testdata"
diff --git a/test/integration/externalresources/rabbitmq_test.go b/test/integration/externalresources/rabbitmq_test.go
index 63bfe2e54..1533a018c 100644
--- a/test/integration/externalresources/rabbitmq_test.go
+++ b/test/integration/externalresources/rabbitmq_test.go
@@ -41,9 +41,6 @@ func TestRabbitMQDeploy(t *testing.T) {
 				testutil.AnnotatedError(t, "CheckPods timed-out %s", err)
 			}
 		}
-		if err := TestHelper.CheckDeployment(ctx, testNamespace, "rabbitmq", 1); err != nil {
-			testutil.AnnotatedErrorf(t, "CheckDeployment  timed-out", "Error validating deployment [%s]: \n%s", "rabbitmq", err)
-		}
 		// inject rabbitmq-client
 		stdout, err := TestHelper.LinkerdRun("inject", "--manual", "testdata/rabbitmq-client.yaml")
 		if err != nil {
@@ -61,9 +58,6 @@ func TestRabbitMQDeploy(t *testing.T) {
 				testutil.AnnotatedError(t, "CheckPods timed-out %s", err)
 			}
 		}
-		if err := TestHelper.CheckDeployment(ctx, testNamespace, "rabbitmq-client", 1); err != nil {
-			testutil.AnnotatedErrorf(t, "CheckDeployment  timed-out", "Error validating deployment [%s]: \n%s", "rabbitmq", err)
-		}
 		// Verify client output
 		golden := "check.rabbitmq.golden"
 		timeout := 50 * time.Second
diff --git a/test/integration/install_test.go b/test/integration/install_test.go
index dc0e7ce04..cdd6faded 100644
--- a/test/integration/install_test.go
+++ b/test/integration/install_test.go
@@ -137,10 +137,6 @@ func TestUpgradeTestAppWorksBeforeUpgrade(t *testing.T) {
 					testutil.AnnotatedError(t, "CheckPods timed-out", err)
 				}
 			}
-
-			if err := TestHelper.CheckDeployment(ctx, testAppNamespace, deploy, 1); err != nil {
-				testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
-			}
 		}
 
 		if err := testutil.ExerciseTestAppEndpoint("/api/list", testAppNamespace, TestHelper); err != nil {
@@ -540,7 +536,7 @@ func TestControlPlaneResourcesPostInstall(t *testing.T) {
 	expectedDeployments := testutil.LinkerdDeployReplicasEdge
 	if !TestHelper.ExternalPrometheus() {
 		expectedServices = append(expectedServices, testutil.Service{Namespace: "linkerd-viz", Name: "prometheus"})
-		expectedDeployments["prometheus"] = testutil.DeploySpec{Namespace: "linkerd-viz", Replicas: 1, Containers: []string{}}
+		expectedDeployments["prometheus"] = testutil.DeploySpec{Namespace: "linkerd-viz", Replicas: 1}
 	}
 
 	// Upgrade Case
@@ -1158,7 +1154,7 @@ func TestCheckProxy(t *testing.T) {
 func TestRestarts(t *testing.T) {
 	expectedDeployments := testutil.LinkerdDeployReplicasEdge
 	if !TestHelper.ExternalPrometheus() {
-		expectedDeployments["prometheus"] = testutil.DeploySpec{Namespace: "linkerd-viz", Replicas: 1, Containers: []string{}}
+		expectedDeployments["prometheus"] = testutil.DeploySpec{Namespace: "linkerd-viz", Replicas: 1}
 	}
 	for deploy, spec := range expectedDeployments {
 		if err := TestHelper.CheckPods(context.Background(), spec.Namespace, deploy, spec.Replicas); err != nil {
diff --git a/test/integration/opaqueports/opaque_ports_test.go b/test/integration/opaqueports/opaque_ports_test.go
index ed3c98d9d..722f2976d 100644
--- a/test/integration/opaqueports/opaque_ports_test.go
+++ b/test/integration/opaqueports/opaque_ports_test.go
@@ -65,10 +65,6 @@ func TestOpaquePorts(t *testing.T) {
 		}
 	}
 
-	if err := TestHelper.CheckDeployment(ctx, opaquePortsNs, opaquePodApp, 1); err != nil {
-		testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", opaquePodApp, err)
-	}
-
 	// Check that the opaque service test application started correctly.
 	if err := TestHelper.CheckPods(ctx, opaquePortsNs, opaqueSvcApp, 1); err != nil {
 		if rce, ok := err.(*testutil.RestartCountError); ok {
@@ -78,10 +74,6 @@ func TestOpaquePorts(t *testing.T) {
 		}
 	}
 
-	if err := TestHelper.CheckDeployment(ctx, opaquePortsNs, opaqueSvcApp, 1); err != nil {
-		testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", opaqueSvcApp, err)
-	}
-
 	// Wait for slow-cookers to start sending requests
 	time.Sleep(20 * time.Second)
 
diff --git a/test/integration/serviceprofiles/serviceprofiles_test.go b/test/integration/serviceprofiles/serviceprofiles_test.go
index 5686382fd..752ee2c04 100644
--- a/test/integration/serviceprofiles/serviceprofiles_test.go
+++ b/test/integration/serviceprofiles/serviceprofiles_test.go
@@ -62,10 +62,6 @@ func testProfiles(t *testing.T) {
 				testutil.AnnotatedError(t, "CheckPods timed-out", err)
 			}
 		}
-
-		if err := TestHelper.CheckDeployment(ctx, testNamespace, deploy, 1); err != nil {
-			testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
-		}
 	}
 
 	testCases := []testCase{
diff --git a/test/integration/smimetrics/smi-metrics_test.go b/test/integration/smimetrics/smi-metrics_test.go
index e7ddf93e3..5c1621e32 100644
--- a/test/integration/smimetrics/smi-metrics_test.go
+++ b/test/integration/smimetrics/smi-metrics_test.go
@@ -68,10 +68,6 @@ func TestSMIMetrics(t *testing.T) {
 		}
 	}
 
-	if err := TestHelper.CheckDeployment(ctx, testNamespace, "smi-metrics", 1); err != nil {
-		testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "error validating deployment [%s]:\n%s", "terminus", err)
-	}
-
 	testCases := []testCase{
 		{
 			name: "linkerd-controller",
diff --git a/test/integration/tap/tap_test.go b/test/integration/tap/tap_test.go
index 168ffedd4..7643ef0fd 100644
--- a/test/integration/tap/tap_test.go
+++ b/test/integration/tap/tap_test.go
@@ -90,10 +90,6 @@ func TestCliTap(t *testing.T) {
 					testutil.AnnotatedError(t, "CheckPods timed-out", err)
 				}
 			}
-
-			if err := TestHelper.CheckDeployment(ctx, prefixedNs, deploy, 1); err != nil {
-				testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
-			}
 		}
 
 		t.Run("tap a deployment", func(t *testing.T) {
diff --git a/test/integration/tracing/tracing_test.go b/test/integration/tracing/tracing_test.go
index 82e691282..dce1fbb48 100644
--- a/test/integration/tracing/tracing_test.go
+++ b/test/integration/tracing/tracing_test.go
@@ -163,10 +163,6 @@ func TestTracing(t *testing.T) {
 				testutil.AnnotatedError(t, "CheckPods timed-out", err)
 			}
 		}
-
-		if err := TestHelper.CheckDeployment(ctx, deploy.ns, deploy.name, 1); err != nil {
-			testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy.name, err)
-		}
 	}
 
 	t.Run("expect full trace", func(t *testing.T) {
diff --git a/test/integration/trafficsplit/trafficsplit_test.go b/test/integration/trafficsplit/trafficsplit_test.go
index 0adde2de8..4b7de2966 100644
--- a/test/integration/trafficsplit/trafficsplit_test.go
+++ b/test/integration/trafficsplit/trafficsplit_test.go
@@ -170,10 +170,6 @@ func TestTrafficSplitCli(t *testing.T) {
 					testutil.AnnotatedError(t, "CheckPods timed-out", err)
 				}
 			}
-
-			if err := TestHelper.CheckDeployment(ctx, prefixedNs, deploy, 1); err != nil {
-				testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
-			}
 		}
 
 		t.Run("ensure traffic is sent to one backend only", func(t *testing.T) {
diff --git a/test/integration/uninstall/uninstall_test.go b/test/integration/uninstall/uninstall_test.go
index b8911c2c5..f90f1fead 100644
--- a/test/integration/uninstall/uninstall_test.go
+++ b/test/integration/uninstall/uninstall_test.go
@@ -73,7 +73,7 @@ func TestResourcesPostInstall(t *testing.T) {
 
 	expectedDeployments := testutil.LinkerdDeployReplicasEdge
 	if !TestHelper.ExternalPrometheus() {
-		expectedDeployments["prometheus"] = testutil.DeploySpec{Namespace: "linkerd-viz", Replicas: 1, Containers: []string{}}
+		expectedDeployments["prometheus"] = testutil.DeploySpec{Namespace: "linkerd-viz", Replicas: 1}
 	}
 	// Upgrade Case
 	if TestHelper.UpgradeHelmFromVersion() != "" {
@@ -87,9 +87,6 @@ func TestResourcesPostInstall(t *testing.T) {
 				testutil.AnnotatedError(t, "CheckPods timed-out", err)
 			}
 		}
-		if err := TestHelper.CheckDeployment(ctx, spec.Namespace, deploy, spec.Replicas); err != nil {
-			testutil.AnnotatedFatalf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
-		}
 	}
 }
 
diff --git a/testutil/install.go b/testutil/install.go
index f776ab7bb..061378543 100644
--- a/testutil/install.go
+++ b/testutil/install.go
@@ -33,9 +33,6 @@ func TestResourcesPostInstall(namespace string, services []Service, deploys map[
 				AnnotatedFatal(t, "CheckPods timed-out", err)
 			}
 		}
-		if err := h.CheckDeployment(ctx, spec.Namespace, deploy, spec.Replicas); err != nil {
-			AnnotatedFatalf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", deploy, err)
-		}
 	}
 }
 
diff --git a/testutil/kubernetes_helper.go b/testutil/kubernetes_helper.go
index ebd961423..8c9015fda 100644
--- a/testutil/kubernetes_helper.go
+++ b/testutil/kubernetes_helper.go
@@ -151,45 +151,6 @@ func (h *KubernetesHelper) Kubectl(stdin string, arg ...string) (string, error)
 	return string(out), err
 }
 
-// getDeployments gets all deployments with a count of their ready replicas in
-// the specified namespace.
-func (h *KubernetesHelper) getDeployments(ctx context.Context, namespace string) (map[string]int, error) {
-	deploys, err := h.clientset.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{})
-	if err != nil {
-		return nil, err
-	}
-
-	deployments := map[string]int{}
-	for _, deploy := range deploys.Items {
-		deployments[deploy.GetName()] = int(deploy.Status.ReadyReplicas)
-	}
-	return deployments, nil
-}
-
-// CheckDeployment checks that a deployment in a namespace contains the expected
-// number of replicas.
-func (h *KubernetesHelper) CheckDeployment(ctx context.Context, namespace string, deploymentName string, replicas int) error {
-	return h.retryFor(30*time.Second, func() error {
-		deploys, err := h.getDeployments(ctx, namespace)
-		if err != nil {
-			return err
-		}
-
-		count, ok := deploys[deploymentName]
-		if !ok {
-			return fmt.Errorf("Deployment [%s] in namespace [%s] not found",
-				deploymentName, namespace)
-		}
-
-		if count != replicas {
-			return fmt.Errorf("Expected deployment [%s] in namespace [%s] to have [%d] replicas, but found [%d]",
-				deploymentName, namespace, replicas, count)
-		}
-
-		return nil
-	})
-}
-
 // GetConfigUID returns the uid associated to the linkerd-config ConfigMap resource
 // in the given namespace
 func (h *KubernetesHelper) GetConfigUID(ctx context.Context, namespace string) (string, error) {
diff --git a/testutil/test_helper.go b/testutil/test_helper.go
index 5fb225084..d264d83a0 100644
--- a/testutil/test_helper.go
+++ b/testutil/test_helper.go
@@ -55,9 +55,8 @@ type helm struct {
 
 // DeploySpec is used to hold information about what deploys we should verify during testing
 type DeploySpec struct {
-	Namespace  string
-	Replicas   int
-	Containers []string
+	Namespace string
+	Replicas  int
 }
 
 // Service is used to hold information about a Service we should verify during testing
@@ -69,14 +68,14 @@ type Service struct {
 // LinkerdDeployReplicasEdge is a map containing the number of replicas for each Deployment and the main
 // container name, in the current code-base
 var LinkerdDeployReplicasEdge = map[string]DeploySpec{
-	"linkerd-controller":     {"linkerd", 1, []string{"public-api"}},
-	"linkerd-destination":    {"linkerd", 1, []string{"destination"}},
-	"tap":                    {"linkerd-viz", 1, []string{"tap"}},
-	"grafana":                {"linkerd-viz", 1, []string{}},
-	"linkerd-identity":       {"linkerd", 1, []string{"identity"}},
-	"linkerd-sp-validator":   {"linkerd", 1, []string{"sp-validator"}},
-	"web":                    {"linkerd-viz", 1, []string{"web"}},
-	"linkerd-proxy-injector": {"linkerd", 1, []string{"proxy-injector"}},
+	"linkerd-controller":     {"linkerd", 1},
+	"linkerd-destination":    {"linkerd", 1},
+	"tap":                    {"linkerd-viz", 1},
+	"grafana":                {"linkerd-viz", 1},
+	"linkerd-identity":       {"linkerd", 1},
+	"linkerd-sp-validator":   {"linkerd", 1},
+	"web":                    {"linkerd-viz", 1},
+	"linkerd-proxy-injector": {"linkerd", 1},
 }
 
 // LinkerdDeployReplicasStable is a map containing the number of replicas for each Deployment and the main
@@ -139,7 +138,7 @@ func NewGenericTestHelper(
 // MulticlusterDeployReplicas is a map containing the number of replicas for each Deployment and the main
 // container name for multicluster components
 var MulticlusterDeployReplicas = map[string]DeploySpec{
-	"linkerd-gateway": {"linkerd-multicluster", 1, []string{"nginx"}},
+	"linkerd-gateway": {"linkerd-multicluster", 1},
 }
 
 // NewTestHelper creates a new instance of TestHelper for the current test run.
diff --git a/web/srv/api_handlers_test.go b/web/srv/api_handlers_test.go
index d67ec0148..69990969c 100644
--- a/web/srv/api_handlers_test.go
+++ b/web/srv/api_handlers_test.go
@@ -78,28 +78,28 @@ func TestHandleApiCheck(t *testing.T) {
 		&healthcheck.CheckResult{
 			Category:    healthcheck.LinkerdConfigChecks,
 			Description: "check3-description",
-			HintAnchor:  "check3-hint-anchor",
+			HintURL:     healthcheck.DefaultHintBaseURL + "check3-hint-anchor",
 			Warning:     false,
 			Err:         nil,
 		},
 		&healthcheck.CheckResult{
 			Category:    healthcheck.LinkerdConfigChecks,
 			Description: "check4-description-kubectl",
-			HintAnchor:  "check4-hint-anchor",
+			HintURL:     healthcheck.DefaultHintBaseURL + "check4-hint-anchor",
 			Warning:     true,
 			Err:         nil,
 		},
 		&healthcheck.CheckResult{
 			Category:    healthcheck.KubernetesAPIChecks,
 			Description: "check1-description",
-			HintAnchor:  "check1-hint-anchor",
+			HintURL:     healthcheck.DefaultHintBaseURL + "check1-hint-anchor",
 			Warning:     false,
 			Err:         nil,
 		},
 		&healthcheck.CheckResult{
 			Category:    healthcheck.KubernetesAPIChecks,
 			Description: "check2-description",
-			HintAnchor:  "check2-hint-anchor",
+			HintURL:     healthcheck.DefaultHintBaseURL + "check2-hint-anchor",
 			Warning:     true,
 			Err:         errors.New("check2-error"),
 		},
diff --git a/web/srv/testdata/api_check_output.json b/web/srv/testdata/api_check_output.json
index f24faa413..b7539ed8c 100644
--- a/web/srv/testdata/api_check_output.json
+++ b/web/srv/testdata/api_check_output.json
@@ -3,14 +3,12 @@
         "kubernetes-api": [{
             "Category": "kubernetes-api",
             "Description": "check1-description",
-            "HintAnchor": "check1-hint-anchor",
             "Retry": false,
             "Warning": false,
             "Err": null
         }, {
             "Category": "kubernetes-api",
             "Description": "check2-description",
-            "HintAnchor": "check2-hint-anchor",
             "Retry": false,
             "Warning": true,
             "Err": {},
@@ -20,7 +18,6 @@
         "linkerd-config": [{
             "Category": "linkerd-config",
             "Description": "check3-description",
-            "HintAnchor": "check3-hint-anchor",
             "Retry": false,
             "Warning": false,
             "Err": null