Repository: linkerd/linkerd2
Pull request #4157 (2020-03-09 20:48:56)

Add alpha clients command
We add the `linkerd alpha clients` command which displays client side metrics from each of a resource's clients.  This allows you to see who all of your clients are and see what your resource's metrics look like from your clients' point of view.  Since these metrics are measured on the client-side, they include network latency.

```
> linkerd alpha clients deploy/web -n emojivoto
FROM                TO   SUCCESS        RPS  LATENCY_P50  LATENCY_P90  LATENCY_P99
vote-bot.emojivoto  web   97.50%     2.0rps          4ms          5ms          5ms
```

adleong: Note: due to a bug in the smi-metrics API controller, clients in different namespaces are not shown.  Once this bug is corrected in the API controller, clients from other namespaces will show up here.

Patch:
diff --git a/.github/workflows/kind_integration.yml b/.github/workflows/kind_integration.yml
index 8810bbf39..a8fea9b52 100644
--- a/.github/workflows/kind_integration.yml
+++ b/.github/workflows/kind_integration.yml
@@ -38,8 +38,7 @@ jobs:
       run: |
         export PATH="`pwd`/bin:$PATH"
         bin/docker-build
-    - name: Create artifact with CLI and image archives (Forked repositories)
-      if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
+    - name: Create artifact with CLI and image archives
       env:
         ARCHIVES: /home/runner/archives
       run: |
@@ -57,8 +56,7 @@ jobs:
     # absolute path is used here.
     #
     # https://github.com/actions/upload-artifact/issues/8
-    - name: Upload artifact (Forked repositories)
-      if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
+    - name: Upload artifact
       # actions/upload-artifact@v1
       uses: actions/upload-artifact@3446296
       with:
diff --git a/.proxy-version b/.proxy-version
index f9993ec65..4b2170c01 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.88.0
+v2.89.0
diff --git a/CHANGES.md b/CHANGES.md
index 70716490a..a88cbb05c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,22 @@
+## edge-20.3.2
+
+This release introduces substantial proxy improvements as well as
+new observability and security functionality.
+
+* CLI
+  * Added the `linkerd alpha stat` command, which uses the smi-metrics
+    API; the latter enables access to metrics to be controlled with RBAC 
+* Controller
+  * Added support for configuring service profile timeouts
+    `(x-linkerd-timeout)` via OpenAPI spec (thanks @lewiscowper!)
+* Web UI
+  * Improved the Grafana dashboards to use a globing operator for Prometheus in
+    order to avoid producing queries that are too large (thanks @mmiller1!)
+* Helm
+  * Improved the `linkerd2` chart README (thanks @lundbird!)
+* Proxy
+  * Fixed a bug that could cause log levels to be processed incorrectly
+
 ## edge-20.3.1
 
 This release introduces new functionality mainly focused around
diff --git a/bin/create-release-tag b/bin/create-release-tag
index 50f5ba9ae..ab95ff9c4 100755
--- a/bin/create-release-tag
+++ b/bin/create-release-tag
@@ -3,41 +3,98 @@
 set -o errexit
 set -o nounset
 
-if [[ $# -ne 1 ]]; then
-    echo "usage: ${0##*/} (edge|stable)-xx.xx.xx" >&2
+# Assert at least one and at most two arguments were passed.
+if [[ $# -lt 1 || $# -gt 2 ]]; then
+    echo "Error: ${0##*/} accepts 1 or 2 arguments" >&2
+    echo "Usage:" >&2
+    echo "    ${0##*/} edge" >&2
+    echo "    ${0##*/} stable x.x.x" >&2
     exit 1
 fi
 
-tag="$1"
-
-# Verify the tag format
-tag_format="^(edge|stable)-([0-9]+)\.([0-9]+)\.([0-9]+)$"
-if [[ $tag =~ $tag_format ]]; then
-    # todo: Use these values to verify the tag version increment.
-    # release_channel="${BASH_REMATCH[1]}"
-    # release_major="${BASH_REMATCH[2]}"
-    # release_minor="${BASH_REMATCH[3]}"
-    # release_patch="${BASH_REMATCH[4]}"
-    :
-else
-    echo "tag format incorrect; expected: $tag_format"
-    echo "example: edge-20.12.2, stable-2.10.1"
+release_channel="$1"
+
+# Verify the release channel.
+release_channel_regex="(edge|stable)"
+if [[ ! $release_channel =~ $release_channel_regex ]]; then
+    echo "Error: valid release channels: edge, stable"
+    echo "Usage:"
+    echo "    bin/create-release-tag edge"
+    echo "    bin/create-release-tag stable 2.4.8"
     exit 1
 fi
 
-# todo: Verify the tag version increment.
+if [ "$release_channel" == "stable" ]; then
+    if [ $# -ne 2 ]; then
+        echo "Error: accepts 2 arguments"
+        echo "Usage:"
+        echo "    bin/create-release-tag stable 2.4.8"
+        exit 1
+    fi
+
+    stable_tag="$2"
+
+    # Verify the stable tag format.
+    stable_tag_regex="^([0-9]+)\.([0-9]+)\.([0-9]+)$"
+    if [[ ! $stable_tag =~ $stable_tag_regex ]]; then
+        echo "Error: version reference incorrect"
+        echo "Usage:"
+        echo "    bin/create-release-tag stable 2.4.8"
+        exit 1
+    fi
+
+    # Set the script global `release_tag` variable.
+    release_tag="stable-$stable_tag"
+fi
+
+if [ "$release_channel" == "edge" ]; then
+if [ $# -ne 1 ]; then
+        echo "Error: accepts 1 argument"
+        echo "Usage:"
+        echo "    bin/create-release-tag edge"
+        exit 1
+    fi
+
+    # Some awks on Linux do no support quantifiers in regex, so this regex is
+    # explicit.
+    edge_tag_regex="(edge)-([0-9][0-9])\.([0-9]|[0-9][0-9])\.([0-9]+)"
+    url="https://run.linkerd.io/install-edge"
+
+    # Get the current edge version.
+    current_edge=$(curl -sL $url | awk -v tag_format="$edge_tag_regex" '$0 ~ tag_format')
+
+    # Get the third and fourth groups of the regex; they are the month and
+    # month minor values for the current edge version.
+    current_mm=$(echo "$current_edge" | sed -n -E "s/.*$edge_tag_regex}$/\3/p")
+    current_xx=$(echo "$current_edge" | sed -n -E "s/.*$edge_tag_regex}$/\4/p")
+
+    yy=$(date +"%y")
+    
+    # Strip leading zero
+    new_mm=$(date +"%-m")
+
+    # If this is a new month, `new_xx` should be 1; otherwise increment it.
+    if [ ! "$new_mm" == "$current_mm" ]; then
+        new_xx="1"
+    else
+        new_xx=$((current_xx+1))
+    fi
+
+    # Set the script global `release_tag` variable.
+    release_tag="edge-$yy.$new_mm.$new_xx"
+fi
 
 bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
 # shellcheck source=bin/_release.sh
 tmp=$(. "$bindir"/_release.sh; extract_release_notes)
 
-# Create an unsigned tag with the commit message.
-git tag -s -F "$tmp" "$tag"
+# Create a signed tag with the commit message.
+git tag -s -F "$tmp" "$release_tag"
 
 # Success message
-echo "tag created and signed."
+echo "$release_tag tag created and signed."
 echo ""
-echo "tag: $tag"
+echo "tag: $release_tag"
 echo ""
 echo "To push tag, run:"
-echo "    git push origin $tag"
+echo "    git push origin $release_tag"
diff --git a/bin/install-pr b/bin/install-pr
new file mode 100755
index 000000000..40445c27c
--- /dev/null
+++ b/bin/install-pr
@@ -0,0 +1,71 @@
+#!/bin/bash
+
+### Install PR ###
+#
+# This script takes a Github pull request number as an argument, downloads the
+# docker images from the pull request's artifacts, pushes them, and installs
+# them on your Kubernetes cluster.  Requires a Github personal access token
+# in the $GITHUB_TOKEN environment variable.
+
+set -eo pipefail
+
+pr=$1
+
+if [ -z "$pr" ]
+then
+  echo "usage: ${0##*/} (PR number)" >&2
+  exit 1
+fi
+
+if [ -z "$GITHUB_TOKEN" ]
+then
+  # shellcheck disable=SC2016
+  echo 'Generate a personal access token at https://github.com/settings/tokens and set it in the $GITHUB_TOKEN env var'
+  exit 1
+fi
+
+auth="Authorization: token $GITHUB_TOKEN"
+
+branch=$(curl -s -H "$auth" "https://api.github.com/repos/linkerd/linkerd2/pulls/$pr" | jq -r '.head.ref')
+
+artifacts=$(curl -s -H "$auth" "https://api.github.com/repos/linkerd/linkerd2/actions/workflows/kind_integration.yml/runs?branch=$branch" | jq -r '.workflow_runs[0].artifacts_url')
+
+archive_url=$(curl -s -H "$auth" "$artifacts" | jq -r '.artifacts[0].archive_download_url')
+
+dir=$(mktemp -d -t "linkerd-pr-$pr.XXXXXXXXXX")
+
+cd "$dir" || exit
+
+echo "### Downloading images ###"
+
+curl -L -o archive.zip -H "$auth" "$archive_url"
+
+unzip -o archive.zip
+
+echo "### Pushing images ###"
+
+for archive in cli-bin cni-plugin controller debug grafana proxy web
+do 
+  image=$(docker load -i "$archive.tar" | sed "s/Loaded image: //")
+  docker push "$image"
+  tag=$(echo "$image" | cut -f 2 -d ":")
+done
+
+cd -
+
+rm -rf "$dir"
+
+linkerd=$(bin/docker-pull-binaries "$tag" | head -n 1)
+
+echo "### Pre checks ###"
+
+$linkerd check --pre
+
+echo "### Installing $tag ###"
+
+$linkerd install | kubectl apply -f -
+
+$linkerd check
+
+echo "### Linkerd installed. CLI available: "
+echo "$linkerd"
diff --git a/bin/shellcheck b/bin/shellcheck
index 622b4bc4f..b05d64e80 100755
--- a/bin/shellcheck
+++ b/bin/shellcheck
@@ -22,7 +22,7 @@ if [ ! -f "$scbin" ]; then
   fi
 
   mkdir -p "$targetbin"
-  wget -qO- "https://storage.googleapis.com/shellcheck/shellcheck-${scversion?}.$file" | tar -OxJv "shellcheck-${scversion}/shellcheck" > "$scbin"
+  curl -sLf "https://storage.googleapis.com/shellcheck/shellcheck-${scversion?}.$file" | tar -OxJv "shellcheck-${scversion}/shellcheck" > "$scbin"
   chmod +x "$scbin"
 fi
 
diff --git a/charts/linkerd2/README.md b/charts/linkerd2/README.md
index d5e1a3400..24d158d3b 100644
--- a/charts/linkerd2/README.md
+++ b/charts/linkerd2/README.md
@@ -85,6 +85,7 @@ The following table lists the configurable parameters of the Linkerd2 chart and
 | Parameter                             | Description                                                                                                                                                                           | Default                              |
 |:--------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------|
 | `controllerImage`                     | Docker image for the controller, tap and identity components                                                                                                                          | `gcr.io/linkerd-io/controller`       |
+| `controllerImageVersion`              | Tag for the controller container docker image                                                                                                                                         | latest version                       |
 | `controllerLogLevel`                  | Log level for the control plane components                                                                                                                                            | `info`                               |
 | `controllerReplicas`                  | Number of replicas for each control plane pod                                                                                                                                         | `1`                                  |
 | `controllerUID`                       | User ID for the control plane components                                                                                                                                              | `2103`                               |
@@ -154,6 +155,7 @@ The following table lists the configurable parameters of the Linkerd2 chart and
 | `tap.keyPEM`                          | Certificate key for Tap component. If not provided then Helm will generate one.                                                                                                                                             ||
 | `webhookFailurePolicy`                | Failure policy for the proxy injector                                                                                                                                                 | `Ignore`                             |
 | `webImage`                            | Docker image for the web container                                                                                                                                                    | `gcr.io/linkerd-io/web`              |
+| `enforcedHostRegexp`                  | Host header validation regex for the dashboard. See the [Linkerd documentation](https://linkerd.io/2/tasks/exposing-dashboard) for more information                                   | `""`              |
 
 ## Add-Ons Configuration
 
diff --git a/charts/linkerd2/values.yaml b/charts/linkerd2/values.yaml
index a8a79454b..256efe04f 100644
--- a/charts/linkerd2/values.yaml
+++ b/charts/linkerd2/values.yaml
@@ -56,7 +56,7 @@ global:
     image:
       name: gcr.io/linkerd-io/proxy-init
       pullPolicy: *image_pull_policy
-      version: v1.1.0
+      version: v1.3.1
     resources:
       cpu:
         limit: 100m
@@ -193,4 +193,3 @@ tracing:
     name: linkerd-jaeger
     image: jaegertracing/all-in-one:1.8
     resources:
-
diff --git a/cli/cmd/alpha.go b/cli/cmd/alpha.go
index 24ec95192..2ca4cd44d 100644
--- a/cli/cmd/alpha.go
+++ b/cli/cmd/alpha.go
@@ -12,6 +12,7 @@ func newCmdAlpha() *cobra.Command {
 	}
 
 	alphaCmd.AddCommand(newCmdAlphaStat())
+	alphaCmd.AddCommand(newCmdAlphaClients())
 
 	return alphaCmd
 }
diff --git a/cli/cmd/alpha_clients.go b/cli/cmd/alpha_clients.go
new file mode 100644
index 000000000..5be4943f9
--- /dev/null
+++ b/cli/cmd/alpha_clients.go
@@ -0,0 +1,81 @@
+package cmd
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/linkerd/linkerd2/controller/api/util"
+	"github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/linkerd/linkerd2/pkg/smimetrics"
+	"github.com/spf13/cobra"
+
+	corev1 "k8s.io/api/core/v1"
+)
+
+type alphaClientsOptions struct {
+	namespace string
+}
+
+func newCmdAlphaClients() *cobra.Command {
+	options := alphaClientsOptions{
+		namespace: corev1.NamespaceDefault,
+	}
+
+	clientsCmd := &cobra.Command{
+		Use:   "clients [flags] (RESOURCE)",
+		Short: "Display client-side traffic stats to a resource.",
+		Long: `Display client-side traffic stats to a resource.
+		
+(RESOURCE) is a named resource of one these kinds:
+  * cronjobs
+  * daemonsets
+  * deployments
+  * jobs
+  * namespaces
+  * pods
+  * replicasets
+  * replicationcontrollers
+  * statefulsets
+
+linkerd alpha clients will return a table of the requested resource's clients
+showing traffic metrics to the requested resource such as request rate, success
+rate, and latency percentiles.  These values are measured on the client side
+and, for example, include network latency.
+
+Examples:
+  linkerd alpha clients -n emojivoto deploy/web`,
+		Args: cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return err
+			}
+
+			target, err := util.BuildResource(options.namespace, args[0])
+			if err != nil {
+				return err
+			}
+			if _, ok := allowedKinds[target.GetType()]; !ok {
+				return fmt.Errorf("%s is not a supported resource type", target.GetType())
+			}
+			if target.GetName() == "" {
+				return errors.New("You must specify a resource name")
+			}
+			kind, err := k8s.PluralResourceNameFromFriendlyName(target.GetType())
+			if err != nil {
+				return err
+			}
+
+			metrics, err := smimetrics.GetTrafficMetricsEdgesList(k8sAPI, options.namespace, kind, target.GetName(), nil)
+			if err != nil {
+				return err
+			}
+			renderTrafficMetricsEdgesList(metrics, stdout, nil, "from")
+
+			return nil
+		},
+	}
+
+	clientsCmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the specified resource")
+	return clientsCmd
+}
diff --git a/cli/cmd/alpha_stat.go b/cli/cmd/alpha_stat.go
index 41f20e679..c45c849f1 100644
--- a/cli/cmd/alpha_stat.go
+++ b/cli/cmd/alpha_stat.go
@@ -119,7 +119,7 @@ Examples:
 					if err != nil {
 						return err
 					}
-					renderTrafficMetricsEdgesList(metrics, stdout, toResource)
+					renderTrafficMetricsEdgesList(metrics, stdout, toResource, "to")
 				} else {
 					metrics, err := smimetrics.GetTrafficMetrics(k8sAPI, target.GetNamespace(), kind, name, nil)
 					if err != nil {
@@ -162,7 +162,7 @@ func buildToResource(namespace, to string) *public.Resource {
 
 func renderTrafficMetrics(metrics *smimetrics.TrafficMetrics, allNamespaces bool, w io.Writer) {
 	t := buildTable(false, allNamespaces)
-	t.Data = []table.Row{metricsToRow(metrics, false)}
+	t.Data = []table.Row{metricsToRow(metrics, "")}
 	t.Render(w)
 }
 
@@ -171,17 +171,17 @@ func renderTrafficMetricsList(metrics *smimetrics.TrafficMetricsList, allNamespa
 	t.Data = []table.Row{}
 	for _, row := range metrics.Items {
 		row := row // Copy to satisfy golint.
-		t.Data = append(t.Data, metricsToRow(&row, false))
+		t.Data = append(t.Data, metricsToRow(&row, ""))
 	}
 	t.Render(w)
 }
 
-func renderTrafficMetricsEdgesList(metrics *smimetrics.TrafficMetricsList, w io.Writer, toResource *public.Resource) {
+func renderTrafficMetricsEdgesList(metrics *smimetrics.TrafficMetricsList, w io.Writer, toResource *public.Resource, direction string) {
 	t := buildTable(true, false)
 	t.Data = []table.Row{}
 	for _, row := range metrics.Items {
 		row := row // Copy to satisfy golint.
-		if row.Edge.Direction != "to" {
+		if row.Edge.Direction != direction {
 			continue
 		}
 		if toResource != nil {
@@ -189,12 +189,8 @@ func renderTrafficMetricsEdgesList(metrics *smimetrics.TrafficMetricsList, w io.
 				log.Debugf("Skipping edge %v", row.Edge.Resource)
 				continue
 			}
-			if toResource.GetNamespace() != "" && row.Edge.Resource.Namespace != toResource.GetNamespace() {
-				log.Debugf("Skipping edge %v", row.Edge.Resource)
-				continue
-			}
 		}
-		t.Data = append(t.Data, metricsToRow(&row, true))
+		t.Data = append(t.Data, metricsToRow(&row, direction))
 	}
 	t.Render(w)
 }
@@ -226,7 +222,7 @@ func getNumericMetricWithUnit(metrics *smimetrics.TrafficMetrics, name string) s
 	return ""
 }
 
-func metricsToRow(metrics *smimetrics.TrafficMetrics, outbound bool) []string {
+func metricsToRow(metrics *smimetrics.TrafficMetrics, direction string) []string {
 	success := getNumericMetric(metrics, "success_count").MilliValue()
 	failure := getNumericMetric(metrics, "failure_count").MilliValue()
 	sr := "-"
@@ -242,15 +238,20 @@ func metricsToRow(metrics *smimetrics.TrafficMetrics, outbound bool) []string {
 	}
 
 	var to string
-	if outbound {
+	var from string
+	if direction == "to" {
 		to = metrics.Edge.Resource.String()
+		from = metrics.Resource.Name
+	} else if direction == "from" {
+		to = metrics.Resource.Name
+		from = metrics.Edge.Resource.String()
 	}
 
 	return []string{
 		metrics.Resource.Namespace,
-		metrics.Resource.Name, // Name
-		metrics.Resource.Name, // From
-		to,                    // To
+		metrics.Resource.Name,
+		from,
+		to,
 		sr,
 		rps,
 		getNumericMetricWithUnit(metrics, "p50_response_latency"),
diff --git a/grafana/dashboards/authority.json b/grafana/dashboards/authority.json
index 08b6caf7f..9e8adedc6 100644
--- a/grafana/dashboards/authority.json
+++ b/grafana/dashboards/authority.json
@@ -1180,7 +1180,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -1202,7 +1202,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/cronjob.json b/grafana/dashboards/cronjob.json
index 8d9a97eb9..0f7268e1c 100644
--- a/grafana/dashboards/cronjob.json
+++ b/grafana/dashboards/cronjob.json
@@ -2174,7 +2174,7 @@
     "templating": {
       "list": [
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {},
           "datasource": "prometheus",
           "definition": "",
@@ -2196,7 +2196,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {},
           "datasource": "prometheus",
           "definition": "",
@@ -2218,7 +2218,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {},
           "datasource": "prometheus",
           "definition": "",
@@ -2240,7 +2240,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {},
           "datasource": "prometheus",
           "definition": "",
diff --git a/grafana/dashboards/daemonset.json b/grafana/dashboards/daemonset.json
index c11e634d0..0a86b717c 100644
--- a/grafana/dashboards/daemonset.json
+++ b/grafana/dashboards/daemonset.json
@@ -2174,7 +2174,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2196,7 +2196,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2218,7 +2218,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2240,7 +2240,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/deployment.json b/grafana/dashboards/deployment.json
index 32185b3f0..08c5d83d7 100644
--- a/grafana/dashboards/deployment.json
+++ b/grafana/dashboards/deployment.json
@@ -2174,7 +2174,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2196,7 +2196,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2218,7 +2218,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2240,7 +2240,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/health.json b/grafana/dashboards/health.json
index 203faf5a6..c613141db 100644
--- a/grafana/dashboards/health.json
+++ b/grafana/dashboards/health.json
@@ -2233,7 +2233,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
@@ -2258,7 +2258,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
@@ -2283,7 +2283,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/job.json b/grafana/dashboards/job.json
index 69bdabd5d..e2e268c07 100644
--- a/grafana/dashboards/job.json
+++ b/grafana/dashboards/job.json
@@ -2174,7 +2174,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2196,7 +2196,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2218,7 +2218,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2240,7 +2240,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/namespace.json b/grafana/dashboards/namespace.json
index 04b82c2dc..3d6a459ab 100644
--- a/grafana/dashboards/namespace.json
+++ b/grafana/dashboards/namespace.json
@@ -893,7 +893,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "label_values(process_start_time_seconds{deployment!=\"\"}, namespace)",
@@ -915,7 +915,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "label_values(process_start_time_seconds{namespace=\"$namespace\"}, deployment)",
diff --git a/grafana/dashboards/pod.json b/grafana/dashboards/pod.json
index 93ffa54b0..1f99773c3 100644
--- a/grafana/dashboards/pod.json
+++ b/grafana/dashboards/pod.json
@@ -2121,7 +2121,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2143,7 +2143,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2165,7 +2165,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "label_values(process_start_time_seconds{namespace=\"$namespace\"}, deployment)",
@@ -2187,7 +2187,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
@@ -2212,7 +2212,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
diff --git a/grafana/dashboards/prometheus-benchmark.json b/grafana/dashboards/prometheus-benchmark.json
index 1a3d36423..c4911aeb5 100644
--- a/grafana/dashboards/prometheus-benchmark.json
+++ b/grafana/dashboards/prometheus-benchmark.json
@@ -3641,7 +3641,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -3663,7 +3663,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/replicaset.json b/grafana/dashboards/replicaset.json
index 59b56bb9b..9564224fc 100644
--- a/grafana/dashboards/replicaset.json
+++ b/grafana/dashboards/replicaset.json
@@ -2208,7 +2208,7 @@
     "templating": {
       "list": [
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {
             "text": "default",
             "value": "default"
@@ -2233,7 +2233,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {
             "text": "rs1",
             "value": "rs1"
@@ -2258,7 +2258,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {
             "text": "All",
             "value": "$__all"
@@ -2283,7 +2283,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {
             "text": "All",
             "value": "$__all"
diff --git a/grafana/dashboards/replicationcontroller.json b/grafana/dashboards/replicationcontroller.json
index 5b4389a69..36c271145 100644
--- a/grafana/dashboards/replicationcontroller.json
+++ b/grafana/dashboards/replicationcontroller.json
@@ -2174,7 +2174,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2196,7 +2196,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2218,7 +2218,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2240,7 +2240,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/service.json b/grafana/dashboards/service.json
index dea9edc23..db517460f 100644
--- a/grafana/dashboards/service.json
+++ b/grafana/dashboards/service.json
@@ -1180,7 +1180,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -1202,7 +1202,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -1224,7 +1224,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
@@ -1249,7 +1249,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
diff --git a/grafana/dashboards/statefulset.json b/grafana/dashboards/statefulset.json
index b9491a996..b6df517ec 100644
--- a/grafana/dashboards/statefulset.json
+++ b/grafana/dashboards/statefulset.json
@@ -2174,7 +2174,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2196,7 +2196,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2218,7 +2218,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2240,7 +2240,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/top-line.json b/grafana/dashboards/top-line.json
index 6483a4773..d9181a2b6 100644
--- a/grafana/dashboards/top-line.json
+++ b/grafana/dashboards/top-line.json
@@ -999,7 +999,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
@@ -1024,7 +1024,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
diff --git a/pkg/profiles/openapi.go b/pkg/profiles/openapi.go
index 661e6c57d..88ad65bd1 100644
--- a/pkg/profiles/openapi.go
+++ b/pkg/profiles/openapi.go
@@ -17,6 +17,7 @@ import (
 
 const (
 	xLinkerdRetryable = "x-linkerd-retryable"
+	xLinkerdTimeout   = "x-linkerd-timeout"
 )
 
 var pathParamRegex = regexp.MustCompile(`\\{[^\}]*\\}`)
@@ -110,9 +111,11 @@ func swaggerToServiceProfile(swagger spec.Swagger, namespace, name, clusterDomai
 
 func mkRouteSpec(path, pathRegex string, method string, operation *spec.Operation) *sp.RouteSpec {
 	retryable := false
+	timeout := ""
 	var responses *spec.Responses
 	if operation != nil {
 		retryable, _ = operation.VendorExtensible.Extensions.GetBool(xLinkerdRetryable)
+		timeout, _ = operation.VendorExtensible.Extensions.GetString(xLinkerdTimeout)
 		responses = operation.Responses
 	}
 	return &sp.RouteSpec{
@@ -120,6 +123,7 @@ func mkRouteSpec(path, pathRegex string, method string, operation *spec.Operatio
 		Condition:       toReqMatch(pathRegex, method),
 		ResponseClasses: toRspClasses(responses),
 		IsRetryable:     retryable,
+		Timeout:         timeout,
 	}
 }
 
diff --git a/pkg/tap/tap.go b/pkg/tap/tap.go
index 1894129a6..80d6b903b 100644
--- a/pkg/tap/tap.go
+++ b/pkg/tap/tap.go
@@ -3,6 +3,7 @@ package tap
 import (
 	"bufio"
 	"bytes"
+	"fmt"
 	"io"
 	"net/http"
 	"net/url"
@@ -37,7 +38,7 @@ func Reader(k8sAPI *k8s.KubernetesAPI, req *pb.TapByResourceRequest, timeout tim
 	if err != nil {
 		return nil, nil, err
 	}
-	url.Path = protohttp.TapReqToURL(req)
+	url.Path = fmt.Sprintf("%s%s", url.Path, protohttp.TapReqToURL(req))
 
 	httpReq, err := http.NewRequest(
 		http.MethodPost,


Test patch:
diff --git a/pkg/profiles/openapi_test.go b/pkg/profiles/openapi_test.go
index 72dbe3aa8..39f419b51 100644
--- a/pkg/profiles/openapi_test.go
+++ b/pkg/profiles/openapi_test.go
@@ -30,7 +30,7 @@ func TestSwaggerToServiceProfile(t *testing.T) {
 									},
 								},
 								VendorExtensible: spec.VendorExtensible{
-									Extensions: spec.Extensions{xLinkerdRetryable: true},
+									Extensions: spec.Extensions{xLinkerdRetryable: true, xLinkerdTimeout: "60s"},
 								},
 							},
 						},
@@ -66,6 +66,7 @@ func TestSwaggerToServiceProfile(t *testing.T) {
 						},
 					},
 					IsRetryable: true,
+					Timeout:     "60s",
 				},
 			},
 		},