Repository: linkerd/linkerd2
Pull request #6673 (2021-08-12 23:12:56)

Add tests regarding pod ownership
https://github.com/linkerd/linkerd2/pull/5567 introduced behavior where we consider the owner references of a pod to be authoritative about which resources contain that pod.  This is in contrast to relying on the label selectors which can be ambiguous when, for example, deployments have overlapping label selectors.

This distinction is non-obvious.  To prevent regressions in this behavior, we add tests to codify this requirement.

Signed-off-by: Alex Leong <alex@buoyant.io>

<!--  Thanks for sending a pull request!

If you already have a well-structured git commit message, chances are GitHub
set the title and description of this PR to the git commit message subject and
body, respectively. If so, you may delete these instructions and submit your PR.

If this is your first time, please read our contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md

The title and description of your Pull Request should match the git commit
subject and body, respectively. Git commit messages are structured as follows:

```
Subject

Problem

Solution

Validation

Fixes #[GitHub issue ID]

DCO Sign off
```

Example git commit message:

```
Introduce Pull Request Template

GitHub's community guidelines recommend a pull request template, the repo was
lacking one.

Introduce a `PULL_REQUEST_TEMPLATE.md` file.

Once merged, the
[Community profile checklist](https://github.com/linkerd/linkerd2/community)
should indicate the repo now provides a pull request template.

Fixes #3321

Signed-off-by: Jane Smith <jane.smith@example.com>
```

Note the git commit message subject becomes the pull request title.

For more details around git commits, see the section on Committing in our
contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md#committing
-->



Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 45c3d5b15..c8129c711 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,7 +13,7 @@ jobs:
     strategy:
       matrix:
         # Keep in sync with integration_tests.yaml matrix build
-        target: [proxy, controller, policy-controller, metrics-api, web, cni-plugin, debug, cli-bin, grafana, jaeger-webhook, tap]
+        target: [proxy, controller, metrics-api, web, cni-plugin, debug, cli-bin, grafana, jaeger-webhook, tap]
     name: Docker build (${{ matrix.target }})
     timeout-minutes: 30
     steps:
@@ -68,6 +68,59 @@ jobs:
       with:
         name: image-archives
         path: /home/runner/archives
+
+  policy_controller_build:
+    runs-on: ubuntu-20.04
+    timeout-minutes: 30
+    strategy:
+      matrix:
+        arch: [amd64, arm64, arm]
+    name: Policy controller build (${{ matrix.arch }})
+    steps:
+    - name: Set up Docker Buildx
+      uses: docker/setup-buildx-action@abe5d8f79a1606a2d3e218847032f3f2b1726ab0
+    - name: Checkout code
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
+    - run: |
+        . bin/_tag.sh
+        echo "TAG=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> $GITHUB_ENV
+    - name: Build ${{ matrix.arch }}
+      run: |
+        echo "${{ secrets.DOCKER_GHCR_PAT }}" | docker login ghcr.io -u "${{ secrets.DOCKER_GHCR_USERNAME }}" --password-stdin
+        docker buildx build --push ./policy-controller/ \
+          -f ./policy-controller/${{ matrix.arch }}.dockerfile \
+          -t ghcr.io/linkerd/policy-controller:$TAG-${{ matrix.arch }}
+
+  policy_controller_manifest:
+    runs-on: ubuntu-20.04
+    timeout-minutes: 30
+    needs: [policy_controller_build]
+    name: Policy controller manifest
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
+    - run: |
+        . bin/_tag.sh
+        echo "TAG=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> $GITHUB_ENV
+    - name: Create multiarch manifest
+      run: |
+        docker manifest create ghcr.io/linkerd/policy-controller:${TAG} \
+            ghcr.io/linkerd/policy-controller:${TAG}-amd64 \
+            ghcr.io/linkerd/policy-controller:${TAG}-arm64 \
+            ghcr.io/linkerd/policy-controller:${TAG}-arm
+    - name: Annotate multiarch manifest
+      run: |
+        docker manifest annotate ghcr.io/linkerd/policy-controller:$TAG \
+            ghcr.io/linkerd/policy-controller:${TAG}-amd64 --os=linux --arch=amd64
+        docker manifest annotate ghcr.io/linkerd/policy-controller:$TAG \
+            ghcr.io/linkerd/policy-controller:${TAG}-arm64 --os=linux --arch=arm64
+        docker manifest annotate ghcr.io/linkerd/policy-controller:$TAG \
+            ghcr.io/linkerd/policy-controller:${TAG}-arm --os=linux --arch=arm
+    - name: Push multiarch manifest
+      run: |
+        echo "${{ secrets.DOCKER_GHCR_PAT }}" | docker login ghcr.io -u "${{ secrets.DOCKER_GHCR_USERNAME }}" --password-stdin
+        docker manifest push ghcr.io/linkerd/policy-controller:$TAG
+
   # todo: Keep in sync with `integration_tests.yml`
   windows_static_cli_tests:
     name: Static CLI tests (windows)
@@ -105,7 +158,7 @@ jobs:
         - upgrade-edge
         - upgrade-stable
         - cni-calico-deep
-    needs: [docker_build]
+    needs: [docker_build, policy_controller_manifest]
     name: Integration tests (${{ matrix.integration_test }})
     timeout-minutes: 60
     runs-on: ubuntu-20.04
@@ -131,11 +184,12 @@ jobs:
         # Validate the CLI version matches the current build tag.
         [[ "$TAG" == "$($CMD version --short --client)" ]]
         bin/tests --images preload --name ${{ matrix.integration_test }} "$CMD"
+
   arm64_integration_tests:
     name: ARM64 integration tests
     timeout-minutes: 60
     runs-on: ubuntu-20.04
-    needs: [docker_build]
+    needs: [docker_build, policy_controller_manifest]
     steps:
     - name: Checkout code
       #if: startsWith(github.ref, 'refs/tags/stable')
@@ -180,12 +234,13 @@ jobs:
       if: ${{ always() }}
       # will fail if other steps didn't run, so ignore error
       run: bin/test-cleanup "$CMD" || true
+
   choco_pack:
     # only runs for stable tags. The conditionals are at each step level instead of the job level
     # otherwise the jobs below that depend on this one won't run
     name: Pack Chocolatey release
     timeout-minutes: 30
-    needs: [integration_tests, arm64_integration_tests]
+    needs: [integration_tests]
     runs-on: windows-2019
     steps:
     - name: Checkout code
@@ -207,6 +262,7 @@ jobs:
       with:
         name: choco
         path: ./linkerd.*.nupkg
+
   gh_release:
     name: Create GH release
     timeout-minutes: 30
@@ -249,6 +305,7 @@ jobs:
           ./target/release/linkerd2-cli-*-linux-*
           ./target/release/linkerd2-cli-*-windows.*
           ./target/release/linkerd2-cli-*.nupkg
+
   website_publish:
     name: Linkerd website publish
     timeout-minutes: 30
@@ -264,6 +321,7 @@ jobs:
         token: ${{ secrets.RELEASE_TOKEN }}
         repository: linkerd/website
         event-type: release
+
   website_publish_check:
     name: Linkerd website publish check
     timeout-minutes: 30
@@ -294,6 +352,7 @@ jobs:
           echo "::error::The version '$TAG' was NOT found published in the website"
           exit 1
         fi
+
   chart_deploy:
     name: Helm chart deploy
     timeout-minutes: 30
diff --git a/bin/docker-build-policy-controller b/bin/docker-build-policy-controller
index 5683e7068..35232938b 100755
--- a/bin/docker-build-policy-controller
+++ b/bin/docker-build-policy-controller
@@ -14,6 +14,11 @@ bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
 # shellcheck source=_tag.sh
 . "$bindir"/_tag.sh
 
+if [ -n "$DOCKER_MULTIARCH" ]; then
+    echo "DOCKER_MULTIARCH may not be set with $0" >&2
+    exit 1
+fi
+
 tag=$(head_root_tag)
 ROOTDIR=$( cd "$bindir"/../policy-controller && pwd )
-docker_build policy-controller "$tag" "$ROOTDIR/Dockerfile"
+docker_build policy-controller "$tag" "$ROOTDIR/amd64.dockerfile"
diff --git a/cli/cmd/repair.go b/cli/cmd/repair.go
index 932a6918f..4af45f222 100644
--- a/cli/cmd/repair.go
+++ b/cli/cmd/repair.go
@@ -97,7 +97,7 @@ func repair(ctx context.Context, forced bool) error {
 	}
 
 	// Load the stored config
-	config, err := k8sAPI.CoreV1().ConfigMaps(controlPlaneNamespace).Get(ctx, "linkerd-config", metav1.GetOptions{})
+	config, err := k8sAPI.CoreV1().ConfigMaps(controlPlaneNamespace).Get(ctx, k8s.ConfigConfigMapName, metav1.GetOptions{})
 	if err != nil {
 		return fmt.Errorf("Failed to load linkerd-config: %s", err)
 	}
diff --git a/go.mod b/go.mod
index 5bf620cb5..b3665967c 100644
--- a/go.mod
+++ b/go.mod
@@ -40,7 +40,7 @@ require (
 	go.opencensus.io v0.23.0
 	golang.org/x/net v0.0.0-20210525063256-abc453219eb5
 	golang.org/x/tools v0.1.5
-	google.golang.org/grpc v1.39.0
+	google.golang.org/grpc v1.40.0
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0
 	google.golang.org/protobuf v1.27.1
 	helm.sh/helm/v3 v3.6.3
diff --git a/go.sum b/go.sum
index dc74f80a1..46e3580a6 100644
--- a/go.sum
+++ b/go.sum
@@ -1317,6 +1317,8 @@ google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
 google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
 google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI=
 google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
diff --git a/multicluster/charts/linkerd-multicluster-link/README.md b/multicluster/charts/linkerd-multicluster-link/README.md
index b74c864b8..20a8457c0 100644
--- a/multicluster/charts/linkerd-multicluster-link/README.md
+++ b/multicluster/charts/linkerd-multicluster-link/README.md
@@ -23,6 +23,7 @@ Kubernetes: `>=1.16.0-0`
 |-----|------|---------|-------------|
 | controllerImage | string | `"cr.l5d.io/linkerd/controller"` | Docker image for the Service mirror component (uses the Linkerd controller image) |
 | controllerImageVersion | string | `"linkerdVersionValue"` | Tag for the Service Mirror container Docker image |
+| enableHeadlessServices | bool | `false` | Toggle support for mirroring headless services |
 | gateway.probe.port | int | `4191` | The port used for liveliness probing |
 | logLevel | string | `"info"` | Log level for the Multicluster components |
 | namespace | string | `"linkerd-multicluster"` | Service Mirror component namespace |
diff --git a/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml b/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml
index 32420020f..31a2ad9e8 100644
--- a/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml
+++ b/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml
@@ -101,6 +101,9 @@ spec:
         - -log-level={{.Values.logLevel}}
         - -event-requeue-limit={{.Values.serviceMirrorRetryLimit}}
         - -namespace={{.Values.namespace}}
+        {{- if .Values.enableHeadlessServices }}
+        - -enable-headless-services
+        {{- end }}
         - {{.Values.targetClusterName}}
         image: {{.Values.controllerImage}}:{{.Values.controllerImageVersion}}
         name: service-mirror
diff --git a/multicluster/charts/linkerd-multicluster-link/values.yaml b/multicluster/charts/linkerd-multicluster-link/values.yaml
index bb6b5e105..ab6a0aedd 100644
--- a/multicluster/charts/linkerd-multicluster-link/values.yaml
+++ b/multicluster/charts/linkerd-multicluster-link/values.yaml
@@ -3,6 +3,8 @@
 controllerImage: cr.l5d.io/linkerd/controller
 # -- Tag for the Service Mirror container Docker image
 controllerImageVersion: linkerdVersionValue
+# -- Toggle support for mirroring headless services
+enableHeadlessServices: false
 gateway:
   probe:
     # -- The port used for liveliness probing
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index 09d14ed00..0d0e9024f 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -259,8 +259,8 @@ const (
 	// ConfigConfigMapName is the name of the ConfigMap containing the linkerd controller configuration.
 	ConfigConfigMapName = "linkerd-config"
 
-	// AddOnsConfigMapName is the name of the ConfigMap containing the linkerd add-ons configuration.
-	AddOnsConfigMapName = "linkerd-config-addons"
+	// DebugContainerName is the name of the default linkerd debug container
+	DebugContainerName = "linkerd-debug"
 
 	// DebugSidecarImage is the image name of the default linkerd debug container
 	DebugSidecarImage = "cr.l5d.io/linkerd/debug"
@@ -294,9 +294,6 @@ const (
 	// IdentityIssuerTrustAnchorsNameExternal is the issuer's certificate file (when using cert-manager).
 	IdentityIssuerTrustAnchorsNameExternal = "ca.crt"
 
-	// IdentityIssuerTrustAnchorsName is the trust anchors name.
-	IdentityIssuerTrustAnchorsName = "ca-bundle.crt"
-
 	// ProxyPortName is the name of the Linkerd Proxy's proxy port.
 	ProxyPortName = "linkerd-proxy"
 
diff --git a/pkg/k8s/portforward.go b/pkg/k8s/portforward.go
index 7dea912f2..554a227b4 100644
--- a/pkg/k8s/portforward.go
+++ b/pkg/k8s/portforward.go
@@ -124,7 +124,17 @@ func newPortForward(
 	emitLogs bool,
 ) (*PortForward, error) {
 
-	req := k8sAPI.CoreV1().RESTClient().Post().
+	restClient := k8sAPI.CoreV1().RESTClient()
+	// This early return is for testing purposes. If the k8sAPI is a fake
+	// client, attempting to create a request will result in a nil-pointer
+	// panic. Instead, we return with no port-forward and no error.
+	if fakeRest, ok := restClient.(*rest.RESTClient); ok {
+		if fakeRest == nil {
+			return nil, nil
+		}
+	}
+
+	req := restClient.Post().
 		Resource("pods").
 		Namespace(namespace).
 		Name(podName).
diff --git a/policy-controller/Cargo.lock b/policy-controller/Cargo.lock
index 2737576db..baff59b1c 100644
--- a/policy-controller/Cargo.lock
+++ b/policy-controller/Cargo.lock
@@ -78,6 +78,12 @@ version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
 
+[[package]]
+name = "bumpalo"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
+
 [[package]]
 name = "bytes"
 version = "1.0.1"
@@ -141,6 +147,15 @@ version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
 
+[[package]]
+name = "ct-logs"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8"
+dependencies = [
+ "sct",
+]
+
 [[package]]
 name = "darling"
 version = "0.12.4"
@@ -495,6 +510,23 @@ dependencies = [
  "want",
 ]
 
+[[package]]
+name = "hyper-rustls"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64"
+dependencies = [
+ "ct-logs",
+ "futures-util",
+ "hyper",
+ "log",
+ "rustls",
+ "rustls-native-certs",
+ "tokio",
+ "tokio-rustls",
+ "webpki",
+]
+
 [[package]]
 name = "hyper-timeout"
 version = "0.4.1"
@@ -566,6 +598,15 @@ version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
 
+[[package]]
+name = "js-sys"
+version = "0.3.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752"
+dependencies = [
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "json-patch"
 version = "0.2.6"
@@ -590,9 +631,9 @@ dependencies = [
 
 [[package]]
 name = "k8s-openapi"
-version = "0.12.0"
+version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbff78f6da26dde0d74188966d23fc763431d730d0f766ecf7699209f8fc243c"
+checksum = "748acc444200aa3528dc131a8048e131a9e75a611a52d152e276e99199313d1a"
 dependencies = [
  "base64",
  "bytes",
@@ -604,9 +645,9 @@ dependencies = [
 
 [[package]]
 name = "kube"
-version = "0.58.1"
+version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21d3c79fb97a822a63ce9422f7302484748032c808954898ba248705e99ea110"
+checksum = "f2bfa22c305a6d817b57a7afcd2e6ee23c80c6c93933edb02f210fdf73f837cc"
 dependencies = [
  "base64",
  "bytes",
@@ -617,6 +658,7 @@ dependencies = [
  "http",
  "http-body",
  "hyper",
+ "hyper-rustls",
  "hyper-timeout",
  "hyper-tls",
  "jsonpath_lib",
@@ -626,6 +668,8 @@ dependencies = [
  "openssl",
  "pem",
  "pin-project 1.0.8",
+ "rustls",
+ "rustls-pemfile",
  "serde",
  "serde_json",
  "serde_yaml",
@@ -636,18 +680,20 @@ dependencies = [
  "tower",
  "tower-http",
  "tracing",
+ "webpki",
 ]
 
 [[package]]
 name = "kube-core"
-version = "0.58.1"
+version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffbce0d890efc42abb31e974419e25a643a97ab7c6b3b9498bda686d10b55f8d"
+checksum = "9c33d2272d8e530938bafc6cf4ac76f2a6f6c9ca684defcfab6c357913a43bcc"
 dependencies = [
  "form_urlencoded",
  "http",
  "json-patch",
  "k8s-openapi",
+ "once_cell",
  "serde",
  "serde_json",
  "thiserror",
@@ -655,9 +701,9 @@ dependencies = [
 
 [[package]]
 name = "kube-derive"
-version = "0.58.1"
+version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95a28be5ca4f233b5dd872f426fdc75d6765c34576b590c44564982c05fdb400"
+checksum = "53dc9fa719dd21d1a4c155cf8936f618a687f3590885e09d9261727cd5dc56a5"
 dependencies = [
  "darling",
  "proc-macro2",
@@ -668,9 +714,9 @@ dependencies = [
 
 [[package]]
 name = "kube-runtime"
-version = "0.58.1"
+version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4f034d330a0849e1603e285389e3f0ed93b9a86017d46e851c9e49e6d0b99e2"
+checksum = "cb40d5730a3ac47b7153c7ad0494a66881bbdf0c17ead478b714dd82c9dba259"
 dependencies = [
  "dashmap",
  "derivative",
@@ -1279,6 +1325,55 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "rustls"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
+dependencies = [
+ "base64",
+ "log",
+ "ring",
+ "sct",
+ "webpki",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092"
+dependencies = [
+ "openssl-probe",
+ "rustls",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
+dependencies = [
+ "base64",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.5"
@@ -1325,6 +1420,16 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
+[[package]]
+name = "sct"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
 [[package]]
 name = "security-framework"
 version = "2.3.1"
@@ -1476,6 +1581,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
 [[package]]
 name = "strsim"
 version = "0.8.0"
@@ -1588,9 +1699,9 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.9.0"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b7b349f11a7047e6d1276853e612d152f5e8a352c61917887cc2169e2366b4c"
+checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b"
 dependencies = [
  "autocfg",
  "bytes",
@@ -1637,6 +1748,17 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "tokio-rustls"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
+dependencies = [
+ "rustls",
+ "tokio",
+ "webpki",
+]
+
 [[package]]
 name = "tokio-stream"
 version = "0.1.7"
@@ -1665,9 +1787,9 @@ dependencies = [
 
 [[package]]
 name = "tonic"
-version = "0.5.0"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b584f064fdfc50017ec39162d5aebce49912f1eb16fd128e04b7f4ce4907c7e5"
+checksum = "796c5e1cd49905e65dd8e700d4cb1dffcbfdb4fc9d017de08c1a537afd83627c"
 dependencies = [
  "async-stream",
  "async-trait",
@@ -1801,6 +1923,17 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "tracing-log"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
+dependencies = [
+ "lazy_static",
+ "log",
+ "tracing-core",
+]
+
 [[package]]
 name = "tracing-subscriber"
 version = "0.2.19"
@@ -1814,6 +1947,7 @@ dependencies = [
  "thread_local",
  "tracing",
  "tracing-core",
+ "tracing-log",
 ]
 
 [[package]]
@@ -1849,6 +1983,12 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
 
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
 [[package]]
 name = "vcpkg"
 version = "0.2.15"
@@ -1883,6 +2023,80 @@ version = "0.10.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
 
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2"
+
+[[package]]
+name = "web-sys"
+version = "0.3.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
 [[package]]
 name = "which"
 version = "4.2.2"
diff --git a/policy-controller/Cargo.toml b/policy-controller/Cargo.toml
index 3d2e0fccf..b8b927835 100644
--- a/policy-controller/Cargo.toml
+++ b/policy-controller/Cargo.toml
@@ -5,19 +5,24 @@ edition = "2018"
 license = "Apache-2.0"
 publish = false
 
+[features]
+default = ["rustls"]
+native-tls = ["kube/native-tls"]
+rustls = ["kube/rustls-tls"]
+
 [dependencies]
 anyhow = "1"
 drain = "0.1"
 futures = "0.3"
 hyper = { version = "0.14", features = ["http1", "http2", "runtime", "server"] }
-kube = { version = "0.58", default-features = false, features = ["client", "derive", "native-tls"] }
+kube = { version = "0.59", default-features = false, features = ["client"] }
 linkerd-policy-controller-core = { path = "./core" }
 linkerd-policy-controller-grpc = { path = "./grpc" }
 linkerd-policy-controller-k8s-index = { path = "./k8s/index" }
 structopt = "0.3"
 tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "parking_lot", "signal", "sync"] }
 tracing = "0.1"
-tracing-subscriber = { version = "0.2", default-features = false, features = ["env-filter", "fmt"] }
+tracing-subscriber = { version = "0.2", default-features = false, features = ["env-filter", "fmt", "tracing-log"] }
 
 [workspace]
 resolver = "2"
diff --git a/policy-controller/Dockerfile b/policy-controller/amd64.dockerfile
similarity index 54%
rename from policy-controller/Dockerfile
rename to policy-controller/amd64.dockerfile
index 4c1277d34..ef425faed 100644
--- a/policy-controller/Dockerfile
+++ b/policy-controller/amd64.dockerfile
@@ -6,9 +6,10 @@ FROM $RUST_IMAGE as build
 ARG TARGETARCH
 WORKDIR /build
 COPY . /build
-RUN target=$(rustup show | sed -n 's/^Default host: \(.*\)/\1/p') ; \
-    cargo build --locked --release --target="$target" --package=linkerd-policy-controller && \
-    mv "target/${target}/release/linkerd-policy-controller" /tmp/
+RUN --mount=type=cache,target=target \
+    --mount=type=cache,from=rust:1.54.0-buster,source=/usr/local/cargo,target=/usr/local/cargo \
+    cargo build --locked --target=x86_64-unknown-linux-gnu --release --package=linkerd-policy-controller && \
+    mv target/x86_64-unknown-linux-gnu/release/linkerd-policy-controller /tmp/
 
 # Creates a minimal runtime image with the controller binary.
 FROM $RUNTIME_IMAGE
diff --git a/policy-controller/arm.dockerfile b/policy-controller/arm.dockerfile
new file mode 100644
index 000000000..fc1cc9c64
--- /dev/null
+++ b/policy-controller/arm.dockerfile
@@ -0,0 +1,19 @@
+ARG RUST_IMAGE=docker.io/library/rust:1.54.0-buster
+ARG RUNTIME_IMAGE=gcr.io/distroless/cc:nonroot
+
+FROM $RUST_IMAGE as build
+RUN apt-get update && \
+    apt-get install -y --no-install-recommends g++-arm-linux-gnueabihf libc6-dev-armhf-cross && \
+    apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/ && \
+    rustup target add armv7-unknown-linux-gnueabihf
+ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
+WORKDIR /build
+COPY . /build
+RUN --mount=type=cache,target=target \
+    --mount=type=cache,from=rust:1.54.0-buster,source=/usr/local/cargo,target=/usr/local/cargo \
+    cargo build --locked --release --target=armv7-unknown-linux-gnueabihf --package=linkerd-policy-controller && \
+    mv target/armv7-unknown-linux-gnueabihf/release/linkerd-policy-controller /tmp/
+
+FROM --platform=linux/arm $RUNTIME_IMAGE
+COPY --from=build /tmp/linkerd-policy-controller /bin/
+ENTRYPOINT ["/bin/linkerd-policy-controller"]
diff --git a/policy-controller/arm64.dockerfile b/policy-controller/arm64.dockerfile
new file mode 100644
index 000000000..6f181d2a7
--- /dev/null
+++ b/policy-controller/arm64.dockerfile
@@ -0,0 +1,19 @@
+ARG RUST_IMAGE=docker.io/library/rust:1.54.0-buster
+ARG RUNTIME_IMAGE=gcr.io/distroless/cc:nonroot
+
+FROM $RUST_IMAGE as build
+RUN apt-get update && \
+    apt-get install -y --no-install-recommends g++-aarch64-linux-gnu libc6-dev-arm64-cross && \
+    apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/ && \
+    rustup target add aarch64-unknown-linux-gnu
+ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
+WORKDIR /build
+COPY . /build
+RUN --mount=type=cache,target=target \
+    --mount=type=cache,from=rust:1.54.0-buster,source=/usr/local/cargo,target=/usr/local/cargo \
+    cargo build --locked --release --target=aarch64-unknown-linux-gnu --package=linkerd-policy-controller && \
+    mv target/aarch64-unknown-linux-gnu/release/linkerd-policy-controller /tmp/
+
+FROM --platform=linux/arm64 $RUNTIME_IMAGE
+COPY --from=build /tmp/linkerd-policy-controller /bin/
+ENTRYPOINT ["/bin/linkerd-policy-controller"]
diff --git a/policy-controller/deny.toml b/policy-controller/deny.toml
index 296ef8fce..8cfca2a92 100644
--- a/policy-controller/deny.toml
+++ b/policy-controller/deny.toml
@@ -15,13 +15,23 @@ ignore = []
 
 [licenses]
 unlicensed = "deny"
-allow = ["Apache-2.0", "BSD-3-Clause", "MIT"]
+allow = ["Apache-2.0", "BSD-3-Clause", "ISC", "MIT"]
 deny = []
 copyleft = "deny"
 allow-osi-fsf-free = "neither"
 default = "deny"
 confidence-threshold = 0.8
-exceptions = []
+exceptions = [
+    { allow = ["ISC", "MIT", "OpenSSL"], name = "ring", version = "*" },
+]
+
+[[licenses.clarify]]
+name = "ring"
+version = "*"
+expression = "MIT AND ISC AND OpenSSL"
+license-files = [
+    { path = "LICENSE", hash = 0xbd0eed23 }
+]
 
 [bans]
 multiple-versions = "deny"
@@ -46,6 +56,3 @@ unknown-registry = "deny"
 unknown-git = "deny"
 allow-registry = ["https://github.com/rust-lang/crates.io-index"]
 allow-git = []
-
-[sources.allow-org]
-github = ["linkerd"]
diff --git a/policy-controller/k8s/api/Cargo.toml b/policy-controller/k8s/api/Cargo.toml
index a63dd461d..90dcdd458 100644
--- a/policy-controller/k8s/api/Cargo.toml
+++ b/policy-controller/k8s/api/Cargo.toml
@@ -7,9 +7,9 @@ publish = false
 
 [dependencies]
 futures = { version = "0.3", default-features = false }
-k8s-openapi = { version = "0.12.0", default-features = false, features = ["v1_20"] }
-kube = { version = "0.58.1", default-features = false, features = ["client", "derive", "native-tls"] }
-kube-runtime = { version = "0.58.1", default-features = false }
+k8s-openapi = { version = "0.13", default-features = false, features = ["v1_16"] }
+kube = { version = "0.59", default-features = false, features = ["derive"] }
+kube-runtime = { version = "0.59", default-features = false }
 schemars = "0.8"
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
diff --git a/policy-controller/k8s/api/src/labels.rs b/policy-controller/k8s/api/src/labels.rs
index e5fd4b6ec..cfdd17321 100644
--- a/policy-controller/k8s/api/src/labels.rs
+++ b/policy-controller/k8s/api/src/labels.rs
@@ -5,7 +5,7 @@ use std::{
     sync::Arc,
 };
 
-#[derive(Clone, Debug, Eq, Default)]
+#[derive(Clone, Debug, Default)]
 pub struct Labels(Arc<Map>);
 
 pub type Map = BTreeMap<String, String>;
@@ -95,6 +95,13 @@ impl std::iter::FromIterator<Expression> for Selector {
 
 // === Labels ===
 
+impl From<Option<Map>> for Labels {
+    #[inline]
+    fn from(labels: Option<Map>) -> Self {
+        labels.unwrap_or_default().into()
+    }
+}
+
 impl From<Map> for Labels {
     #[inline]
     fn from(labels: Map) -> Self {
@@ -109,13 +116,23 @@ impl AsRef<Map> for Labels {
     }
 }
 
-impl<T: AsRef<Map>> std::cmp::PartialEq<T> for Labels {
+impl std::cmp::PartialEq<Self> for Labels {
     #[inline]
-    fn eq(&self, t: &T) -> bool {
+    fn eq(&self, t: &Self) -> bool {
         self.0.as_ref().eq(t.as_ref())
     }
 }
 
+impl std::cmp::PartialEq<Option<Map>> for Labels {
+    #[inline]
+    fn eq(&self, t: &Option<Map>) -> bool {
+        match t {
+            None => self.0.is_empty(),
+            Some(t) => t.eq(self.0.as_ref()),
+        }
+    }
+}
+
 impl std::iter::FromIterator<(String, String)> for Labels {
     fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
         Self(Arc::new(iter.into_iter().collect()))
diff --git a/policy-controller/k8s/index/src/default_allow.rs b/policy-controller/k8s/index/src/default_allow.rs
index 8e007de7b..5e89efb75 100644
--- a/policy-controller/k8s/index/src/default_allow.rs
+++ b/policy-controller/k8s/index/src/default_allow.rs
@@ -33,12 +33,14 @@ impl DefaultAllow {
     pub const ANNOTATION: &'static str = "policy.linkerd.io/default-allow";
 
     pub fn from_annotation(meta: &k8s::ObjectMeta) -> Result<Option<Self>> {
-        if let Some(v) = meta.annotations.get(Self::ANNOTATION) {
-            let mode = v.parse()?;
-            Ok(Some(mode))
-        } else {
-            Ok(None)
+        if let Some(ann) = meta.annotations.as_ref() {
+            if let Some(v) = ann.get(Self::ANNOTATION) {
+                let mode = v.parse()?;
+                return Ok(Some(mode));
+            }
         }
+
+        Ok(None)
     }
 }
 
diff --git a/policy-controller/k8s/index/src/node.rs b/policy-controller/k8s/index/src/node.rs
index 7ecc72f27..859eba610 100644
--- a/policy-controller/k8s/index/src/node.rs
+++ b/policy-controller/k8s/index/src/node.rs
@@ -206,17 +206,17 @@ impl KubeletIps {
     fn try_from_node(node: k8s::Node) -> Result<Self> {
         let spec = node.spec.ok_or_else(|| anyhow!("node missing spec"))?;
 
-        let addrs = if spec.pod_cidrs.is_empty() {
+        let addrs = if let Some(cidrs) = spec.pod_cidrs {
+            cidrs
+                .into_iter()
+                .map(Self::try_from_cidr)
+                .collect::<Result<Vec<_>>>()?
+        } else {
             let cidr = spec
                 .pod_cidr
                 .ok_or_else(|| anyhow!("node missing pod_cidr"))?;
             let ip = Self::try_from_cidr(cidr)?;
             vec![ip]
-        } else {
-            spec.pod_cidrs
-                .into_iter()
-                .map(Self::try_from_cidr)
-                .collect::<Result<Vec<_>>>()?
         };
 
         Ok(Self(addrs.into()))
diff --git a/policy-controller/k8s/index/src/pod.rs b/policy-controller/k8s/index/src/pod.rs
index fea040441..7a67e5229 100644
--- a/policy-controller/k8s/index/src/pod.rs
+++ b/policy-controller/k8s/index/src/pod.rs
@@ -260,7 +260,7 @@ impl PodIndex {
                 // labels have changed, then we relink servers to pods in case label selections have
                 // changed.
                 let p = entry.get_mut();
-                if p.labels.as_ref() != &pod.metadata.labels {
+                if p.labels != pod.metadata.labels {
                     p.labels = pod.metadata.labels.into();
                     p.link_servers(servers);
                 }
@@ -281,7 +281,7 @@ impl PodIndex {
         let mut lookups = HashMap::new();
 
         for container in spec.containers.into_iter() {
-            for p in container.ports.into_iter() {
+            for p in container.ports.into_iter().flatten() {
                 if p.protocol.map(|p| p == "TCP").unwrap_or(true) {
                     let port = p.container_port as u16;
                     if ports.by_port.contains_key(&port) {
diff --git a/policy-controller/k8s/index/src/server.rs b/policy-controller/k8s/index/src/server.rs
index 93c6561ea..71333ce1f 100644
--- a/policy-controller/k8s/index/src/server.rs
+++ b/policy-controller/k8s/index/src/server.rs
@@ -210,7 +210,7 @@ impl SrvIndex {
             HashEntry::Occupied(mut entry) => {
                 // If something about the server changed, we need to update the config to reflect
                 // the change.
-                let new_labels = if entry.get().labels.as_ref() != &srv.metadata.labels {
+                let new_labels = if entry.get().labels != srv.metadata.labels {
                     Some(k8s::Labels::from(srv.metadata.labels))
                 } else {
                     None
diff --git a/viz/cmd/stat.go b/viz/cmd/stat.go
index c00e580c9..f711f7ed3 100644
--- a/viz/cmd/stat.go
+++ b/viz/cmd/stat.go
@@ -150,6 +150,18 @@ If no resource name is specified, displays stats about all resources of the spec
   # Get all pods in all namespaces that call the hello1 service in the test namespace.
   linkerd viz stat pods --to svc/hello1 --to-namespace test --all-namespaces
 
+  # Get the web service. With Services, metrics are generated from the outbound metrics
+  # of clients, and thus will not include unmeshed client request metrics.
+  linkerd viz stat svc/web
+
+  # Get the web services and metrics for any traffic coming to the service from the hello1 deployment
+  # in the test namespace.
+  linkerd viz stat svc/web --from deploy/hello1 --from-namespace test
+
+  # Get the web services and metrics for all the traffic that reaches the web-pod1 pod
+  # in the test namespace exclusively.
+  linkerd viz stat svc/web --to pod/web-pod1 --to-namespace test
+
   # Get all services in all namespaces that receive calls from hello1 deployment in the test namespace.
   linkerd viz stat services --from deploy/hello1 --from-namespace test --all-namespaces
 
diff --git a/web/app/js/components/ServiceMesh.jsx b/web/app/js/components/ServiceMesh.jsx
index 7e0977626..bc09a578d 100644
--- a/web/app/js/components/ServiceMesh.jsx
+++ b/web/app/js/components/ServiceMesh.jsx
@@ -34,6 +34,17 @@ const styles = {
   },
 };
 
+const installedExtensionsColumn = [
+  {
+    title: <Trans>columnTitleName</Trans>,
+    dataIndex: 'name',
+  },
+  {
+    title: <Trans>columnTitleNamespace</Trans>,
+    dataIndex: 'namespace',
+  },
+];
+
 const serviceMeshDetailsColumns = [
   {
     title: <Trans>columnTitleName</Trans>,
@@ -72,6 +83,7 @@ class ServiceMesh extends React.Component {
     this.state = {
       pollingInterval: 2000,
       components: [],
+      extensions: [],
       nsStatuses: [],
       pendingRequests: false,
       loaded: false,
@@ -81,6 +93,7 @@ class ServiceMesh extends React.Component {
 
   componentDidMount() {
     this.startServerPolling();
+    this.fetchAllInstalledExtensions();
   }
 
   componentDidUpdate(prevProps) {
@@ -109,6 +122,12 @@ class ServiceMesh extends React.Component {
     ];
   }
 
+  getInstalledExtensions() {
+    const { extensions } = this.state;
+    const extensionList = !_isEmpty(extensions.extensions) ? extensions.extensions : [];
+    return extensionList;
+  }
+
   getControllerComponentData = podData => {
     const podDataByDeploy = _groupBy(_filter(podData.pods, d => d.controlPlane), p => p.deployment);
     const byDeployName = _mapKeys(podDataByDeploy, (_pods, dep) => dep.split('/')[1]);
@@ -190,6 +209,15 @@ class ServiceMesh extends React.Component {
       .catch(this.handleApiError);
   }
 
+  fetchAllInstalledExtensions() {
+    this.api.setCurrentRequests([this.api.fetchExtension()]);
+    this.serverPromise = Promise.all(this.api.getCurrentPromises())
+      .then(([extensions]) => {
+        this.setState({ extensions });
+      })
+      .catch(this.handleApiError);
+  }
+
   handleApiError(e) {
     if (e.isCanceled) {
       return;
@@ -234,6 +262,23 @@ class ServiceMesh extends React.Component {
     );
   }
 
+  renderInstalledExtensions() {
+    return (
+      <React.Fragment>
+        <Grid container justify="space-between">
+          <Grid item xs={3}>
+            <Typography variant="h6"><Trans>Installed Extensions</Trans></Typography>
+          </Grid>
+        </Grid>
+        <BaseTable
+          tableClassName="metric-table"
+          tableRows={this.getInstalledExtensions()}
+          tableColumns={installedExtensionsColumn}
+          rowKey={d => d.uid} />
+      </React.Fragment>
+    );
+  }
+
   renderServiceMeshDetails() {
     return (
       <React.Fragment>
@@ -298,6 +343,7 @@ class ServiceMesh extends React.Component {
             <Grid container spacing={3}>
               <Grid item xs={8} container direction="column">
                 <Grid item>{this.renderControlPlaneDetails()}</Grid>
+                <Grid item>{this.renderInstalledExtensions()}</Grid>
                 <Grid item>
                   <MeshedStatusTable tableRows={nsStatuses} />
                 </Grid>
diff --git a/web/app/js/components/util/ApiHelpers.jsx b/web/app/js/components/util/ApiHelpers.jsx
index 534761539..58371fb08 100644
--- a/web/app/js/components/util/ApiHelpers.jsx
+++ b/web/app/js/components/util/ApiHelpers.jsx
@@ -61,7 +61,7 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
   const servicesPath = '/api/services';
   const edgesPath = '/api/edges';
   const gatewaysPath = '/api/gateways';
-  const l5dExtensionsPath = '/api/extension';
+  const l5dExtensionsPath = '/api/extensions';
 
   const validMetricsWindows = {
     '10s': '10 minutes',
@@ -147,7 +147,11 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
   };
 
   const fetchExtension = name => {
-    return apiFetch(`${l5dExtensionsPath}?extension_name=${name}`);
+    let extensionPath = l5dExtensionsPath;
+    if (name) {
+      extensionPath += `?extension_name=${name}`;
+    }
+    return apiFetch(extensionPath);
   };
 
   const fetchResourceDefinition = (namespace, resourceType, resourceName) => {
diff --git a/web/app/package.json b/web/app/package.json
index ca761c877..ab6a56fe5 100644
--- a/web/app/package.json
+++ b/web/app/package.json
@@ -46,7 +46,7 @@
     "@babel/plugin-proposal-class-properties": "^7.13.0",
     "@babel/preset-env": "^7.15.0",
     "@babel/preset-react": "^7.14.5",
-    "@babel/runtime": "^7.14.8",
+    "@babel/runtime": "^7.15.3",
     "@lingui/cli": "3.10.2",
     "babel-core": "^7.0.0-bridge.0",
     "babel-jest": "^27.0.6",
@@ -78,7 +78,7 @@
     "sinon-stub-promise": "4.0.0",
     "style-loader": "^3.2.1",
     "url-loader": "^4.1.1",
-    "webpack": "^5.49.0",
+    "webpack": "^5.50.0",
     "webpack-bundle-analyzer": "4.4.2",
     "webpack-cli": "4.7.2",
     "webpack-dev-server": "3.11.2"
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index ba48615cf..aceef00fe 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -955,10 +955,10 @@
     core-js-pure "^3.15.0"
     regenerator-runtime "^0.13.4"
 
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
-  version "7.14.8"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446"
-  integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
+  version "7.15.3"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b"
+  integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==
   dependencies:
     regenerator-runtime "^0.13.4"
 
@@ -9252,9 +9252,9 @@ url-loader@^4.1.1:
     schema-utils "^3.0.0"
 
 url-parse@^1.4.3, url-parse@^1.5.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b"
-  integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==
+  version "1.5.3"
+  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862"
+  integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==
   dependencies:
     querystringify "^2.1.1"
     requires-port "^1.0.0"
@@ -9521,10 +9521,10 @@ webpack-sources@^3.2.0:
   resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d"
   integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==
 
-webpack@^5.49.0:
-  version "5.49.0"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.49.0.tgz#e250362b781a9fb614ba0a97ed67c66b9c5310cd"
-  integrity sha512-XarsANVf28A7Q3KPxSnX80EkCcuOer5hTOEJWJNvbskOZ+EK3pobHarGHceyUZMxpsTHBHhlV7hiQyLZzGosYw==
+webpack@^5.50.0:
+  version "5.50.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.50.0.tgz#5562d75902a749eb4d75131f5627eac3a3192527"
+  integrity sha512-hqxI7t/KVygs0WRv/kTgUW8Kl3YC81uyWQSo/7WUs5LsuRw0htH/fCwbVBGCuiX/t4s7qzjXFcf41O8Reiypag==
   dependencies:
     "@types/eslint-scope" "^3.7.0"
     "@types/estree" "^0.0.50"
diff --git a/web/srv/api_handlers.go b/web/srv/api_handlers.go
index 0e2411de8..5e73fa3e8 100644
--- a/web/srv/api_handlers.go
+++ b/web/srv/api_handlers.go
@@ -434,23 +434,54 @@ func (h *handler) handleAPIResourceDefinition(w http.ResponseWriter, req *http.R
 	w.Write(resourceDefinition)
 }
 
-func (h *handler) handleGetExtension(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
+func (h *handler) handleGetExtensions(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
 	ctx := req.Context()
 	extensionName := req.FormValue("extension_name")
 
+	type Extension struct {
+		Name      string `json:"name"`
+		UID       string `json:"uid"`
+		Namespace string `json:"namespace"`
+	}
+
 	resp := map[string]interface{}{}
-	ns, err := h.k8sAPI.GetNamespaceWithExtensionLabel(ctx, extensionName)
-	if err != nil && strings.HasPrefix(err.Error(), "could not find") {
+	if extensionName != "" {
+		ns, err := h.k8sAPI.GetNamespaceWithExtensionLabel(ctx, extensionName)
+		if err != nil && strings.HasPrefix(err.Error(), "could not find") {
+			renderJSON(w, resp)
+			return
+		} else if err != nil {
+			renderJSONError(w, err, http.StatusInternalServerError)
+			return
+		}
+
+		resp["data"] = Extension{
+			UID:       string(ns.UID),
+			Name:      ns.GetLabels()[k8s.LinkerdExtensionLabel],
+			Namespace: ns.Name,
+		}
+
 		renderJSON(w, resp)
 		return
-	} else if err != nil {
+	}
+
+	installedExtensions, err := h.k8sAPI.GetAllNamespacesWithExtensionLabel(ctx)
+	if err != nil {
 		renderJSONError(w, err, http.StatusInternalServerError)
 		return
 	}
 
-	resp["extensionName"] = ns.GetLabels()[k8s.LinkerdExtensionLabel]
-	resp["namespace"] = ns.Name
+	extensionList := make([]Extension, len(installedExtensions))
+
+	for i, installedExtension := range installedExtensions {
+		extensionList[i] = Extension{
+			UID:       string(installedExtension.GetObjectMeta().GetUID()),
+			Name:      installedExtension.GetLabels()[k8s.LinkerdExtensionLabel],
+			Namespace: installedExtension.GetName(),
+		}
+	}
 
+	resp["extensions"] = extensionList
 	renderJSON(w, resp)
 }
 
diff --git a/web/srv/server.go b/web/srv/server.go
index 8219b3c62..294699160 100644
--- a/web/srv/server.go
+++ b/web/srv/server.go
@@ -196,7 +196,7 @@ func NewServer(
 	server.router.GET("/api/check", handler.handleAPICheck)
 	server.router.GET("/api/resource-definition", handler.handleAPIResourceDefinition)
 	server.router.GET("/api/gateways", handler.handleAPIGateways)
-	server.router.GET("/api/extension", handler.handleGetExtension)
+	server.router.GET("/api/extensions", handler.handleGetExtensions)
 
 	// grafana proxy
 	server.handleAllOperationsForPath("/grafana/*grafanapath", handler.handleGrafana)


Test patch:
diff --git a/cli/cmd/metrics_test.go b/cli/cmd/metrics_test.go
new file mode 100644
index 000000000..d28cda687
--- /dev/null
+++ b/cli/cmd/metrics_test.go
@@ -0,0 +1,130 @@
+package cmd
+
+import (
+	"context"
+	"testing"
+
+	"github.com/linkerd/linkerd2/pkg/k8s"
+)
+
+func TestGetPodsFor(t *testing.T) {
+
+	configs := []string{
+		// pod-1
+		`apiVersion: v1
+kind: Pod
+metadata:
+  name: pod-1
+  namespace: ns
+  uid: pod-1
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: ReplicaSet
+    name: rs-1
+    uid: rs-1
+`,
+		// rs-1
+		`apiVersion: apps/v1
+kind: ReplicaSet
+metadata:
+  name: rs-1
+  namespace: ns
+  uid: rs-1
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: Deployment
+    name: deploy-1
+    uid: deploy-1
+spec:
+  selector:
+    matchLabels:
+      app: foo
+`,
+		// deploy-1
+		`apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: deploy-1
+  namespace: ns
+  uid: deploy-1
+spec:
+  selector:
+    matchLabels:
+      app: foo
+`,
+		// pod-2
+		`apiVersion: v1
+kind: Pod
+metadata:
+  name: pod-2
+  namespace: ns
+  uid: pod-2
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: ReplicaSet
+    name: rs-2
+    uid: rs-2
+`,
+		// rs-2
+		`apiVersion: apps/v1
+kind: ReplicaSet
+metadata:
+  name: rs-2
+  namespace: ns
+  uid: rs-2
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: Deployment
+    name: deploy-2
+    uid: deploy-2
+spec:
+  selector:
+    matchLabels:
+     app: foo
+`,
+		// deploy-2
+		`apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: deploy-2
+  namespace: ns
+  uid: deploy-2
+spec:
+  selector:
+    matchLabels:
+      app: foo
+`}
+
+	k8sClient, err := k8s.NewFakeAPI(configs...)
+	if err != nil {
+		t.Fatalf("Unexpected error %s", err)
+	}
+
+	// Both pod-1 and pod-2 have labels which match deploy-1's selector.
+	// However, only pod-1 is owned by deploy-1 according to the owner references.
+	// Owner references should be considered authoritative to resolve ambiguity
+	// when deployments have overlapping seletors.
+	pods, err := getPodsFor(context.Background(), k8sClient, "ns", "deploy/deploy-1")
+	if err != nil {
+		t.Fatalf("Unexpected error %s", err)
+	}
+
+	if len(pods) != 1 {
+		for _, p := range pods {
+			t.Logf("%s/%s", p.Namespace, p.Name)
+		}
+		t.Fatalf("Expected 1 pod, got %d", len(pods))
+	}
+}
diff --git a/pkg/k8s/portforward_test.go b/pkg/k8s/portforward_test.go
index 989070e81..e1a169b98 100644
--- a/pkg/k8s/portforward_test.go
+++ b/pkg/k8s/portforward_test.go
@@ -69,57 +69,177 @@ spec:
 }
 
 func TestNewPortForward(t *testing.T) {
-	// TODO: test successful cases by mocking out `clientset.CoreV1().RESTClient()`
 	tests := []struct {
-		ns         string
-		deployName string
-		k8sConfigs []string
-		err        error
+		description string
+		ns          string
+		deployName  string
+		k8sConfigs  []string
+		err         error
 	}{
 		{
-			"pod-ns",
-			"deploy-name",
+			"Pod is owned by the specified deployment",
+			"ns",
+			"deploy",
 			[]string{`apiVersion: v1
 kind: Pod
 metadata:
-  name: bad-name
-  namespace: pod-ns
+  name: pod
+  namespace: ns
+  uid: pod
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: Deployment
+    name: rs
+    uid: rs
 status:
   phase: Running`,
-			},
-			errors.New("no running pods found for deploy-name"),
+				`apiVersion: apps/v1
+kind: ReplicaSet
+metadata:
+  name: rs
+  namespace: ns
+  uid: rs
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: Deployment
+    name: deploy
+    uid: deploy
+  spec:
+    selector:
+      matchLabels:
+        app: foo
+`,
+				`apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: deploy
+  namespace: ns
+  uid: deploy
+spec:
+  selector:
+    matchLabels:
+      app: foo
+`},
+			nil,
 		},
+		// In the case of overlapping deployments, a pod may match the label
+		// selector of more than one deployment but will still be owned by
+		// exactly one.
 		{
-			"pod-ns",
-			"deploy-name",
+			"Pod's labels match, but is not owned by the deployment",
+			"ns",
+			"deploy",
 			[]string{`apiVersion: v1
 kind: Pod
 metadata:
-  name: deploy-name-foo-bar
-  namespace: bad-ns
-status:
-  phase: Running`,
-			},
-			errors.New("no running pods found for deploy-name"),
+  name: pod
+  namespace: ns
+  uid: pod
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: ReplicaSet
+    name: rs
+    uid: SOME-OTHER-UID
+  status:
+    phase: Running`,
+				`apiVersion: apps/v1
+kind: ReplicaSet
+metadata:
+  name: rs
+  namespace: ns
+  uid: rs
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: Deployment
+    name: deploy
+    uid: deploy
+  spec:
+    selector:
+      matchLabels:
+        app: foo
+`,
+				`apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: deploy
+  namespace: ns
+  uid: deploy
+spec:
+  selector:
+    matchLabels:
+      app: foo
+`},
+			errors.New("no running pods found for deploy"),
 		},
 		{
-			"pod-ns",
-			"deploy-name",
+			"Pod is owned by the specified deployment but is not running",
+			"ns",
+			"deploy",
 			[]string{`apiVersion: v1
 kind: Pod
 metadata:
-  name: deploy-name-foo-bar
-  namespace: pod-ns
-status:
-  phase: Stopped`,
-			},
-			errors.New("no running pods found for deploy-name"),
+  name: pod
+  namespace: ns
+  uid: pod
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: ReplicaSet
+    name: rs
+    uid: rs
+  status:
+    phase: Stopped`,
+				`apiVersion: apps/v1
+kind: ReplicaSet
+metadata:
+  name: rs
+  namespace: ns
+  uid: rs
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: Deployment
+    name: deploy
+    uid: deploy
+spec:
+  selector:
+    matchLabels:
+      app: foo
+`,
+				`apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: deploy
+  namespace: ns
+  uid: deploy
+spec:
+  selector:
+    matchLabels:
+      app: foo
+`},
+			errors.New("no running pods found for deploy"),
 		},
 	}
 
-	for i, test := range tests {
+	for _, test := range tests {
 		test := test // pin
-		t.Run(fmt.Sprintf("%d: NewPortForward returns expected result", i), func(t *testing.T) {
+		t.Run(test.description, func(t *testing.T) {
 			k8sClient, err := NewFakeAPI(test.k8sConfigs...)
 			if err != nil {
 				t.Fatalf("Unexpected error %s", err)
diff --git a/policy-controller/k8s/index/src/tests.rs b/policy-controller/k8s/index/src/tests.rs
index 99cdb569d..534f34cc9 100644
--- a/policy-controller/k8s/index/src/tests.rs
+++ b/policy-controller/k8s/index/src/tests.rs
@@ -462,7 +462,7 @@ fn mk_node(name: impl Into<String>, pod_net: IpNet) -> k8s::Node {
         },
         spec: Some(k8s::api::core::v1::NodeSpec {
             pod_cidr: Some(pod_net.to_string()),
-            pod_cidrs: vec![pod_net.to_string()],
+            pod_cidrs: Some(vec![pod_net.to_string()]),
             ..Default::default()
         }),
         status: Some(k8s::api::core::v1::NodeStatus::default()),
@@ -488,22 +488,24 @@ fn mk_pod(
                 .into_iter()
                 .map(|(name, ports)| k8s::api::core::v1::Container {
                     name: name.into(),
-                    ports: ports
-                        .into_iter()
-                        .map(|p| k8s::api::core::v1::ContainerPort {
-                            container_port: p as i32,
-                            ..Default::default()
-                        })
-                        .collect(),
+                    ports: Some(
+                        ports
+                            .into_iter()
+                            .map(|p| k8s::api::core::v1::ContainerPort {
+                                container_port: p as i32,
+                                ..Default::default()
+                            })
+                            .collect(),
+                    ),
                     ..Default::default()
                 })
                 .collect(),
             ..Default::default()
         }),
         status: Some(k8s::api::core::v1::PodStatus {
-            pod_ips: vec![k8s::api::core::v1::PodIP {
+            pod_ips: Some(vec![k8s::api::core::v1::PodIP {
                 ip: Some(pod_ip.to_string()),
-            }],
+            }]),
             ..Default::default()
         }),
     }
@@ -522,10 +524,12 @@ fn mk_server(
         metadata: k8s::ObjectMeta {
             namespace: Some(ns.into()),
             name: Some(name.into()),
-            labels: srv_labels
-                .into_iter()
-                .map(|(k, v)| (k.to_string(), v.to_string()))
-                .collect(),
+            labels: Some(
+                srv_labels
+                    .into_iter()
+                    .map(|(k, v)| (k.to_string(), v.to_string()))
+                    .collect(),
+            ),
             ..Default::default()
         },
         spec: k8s::policy::ServerSpec {
diff --git a/testutil/inject_validator.go b/testutil/inject_validator.go
index 0fa7c0ee6..270c47c52 100644
--- a/testutil/inject_validator.go
+++ b/testutil/inject_validator.go
@@ -12,9 +12,6 @@ import (
 )
 
 const enabled = "true"
-const proxyContainerName = "linkerd-proxy"
-const initContainerName = "linkerd-init"
-const debugContainerName = "linkerd-debug"
 
 // InjectValidator is used as a helper to generate
 // correct injector flags and annotations and verify
@@ -91,18 +88,18 @@ func (iv *InjectValidator) validatePort(container *v1.Container, portName string
 
 func (iv *InjectValidator) validateDebugContainer(pod *v1.PodSpec) error {
 	if iv.EnableDebug {
-		proxyContainer := iv.getContainer(pod, debugContainerName, false)
+		proxyContainer := iv.getContainer(pod, k8s.DebugContainerName, false)
 		if proxyContainer == nil {
-			return fmt.Errorf("container %s missing", debugContainerName)
+			return fmt.Errorf("container %s missing", k8s.DebugContainerName)
 		}
 	}
 	return nil
 }
 
 func (iv *InjectValidator) validateProxyContainer(pod *v1.PodSpec) error {
-	proxyContainer := iv.getContainer(pod, proxyContainerName, false)
+	proxyContainer := iv.getContainer(pod, k8s.ProxyContainerName, false)
 	if proxyContainer == nil {
-		return fmt.Errorf("container %s missing", proxyContainerName)
+		return fmt.Errorf("container %s missing", k8s.ProxyContainerName)
 	}
 
 	if iv.AdminPort != 0 {
@@ -116,7 +113,7 @@ func (iv *InjectValidator) validateProxyContainer(pod *v1.PodSpec) error {
 			return fmt.Errorf("readinessProbe: expected: %d, actual %d", iv.AdminPort, proxyContainer.LivenessProbe.HTTPGet.Port.IntVal)
 		}
 
-		if err := iv.validatePort(proxyContainer, "linkerd-admin", iv.AdminPort); err != nil {
+		if err := iv.validatePort(proxyContainer, k8s.ProxyAdminPortName, iv.AdminPort); err != nil {
 			return err
 		}
 	}
@@ -149,7 +146,7 @@ func (iv *InjectValidator) validateProxyContainer(pod *v1.PodSpec) error {
 		if proxyContainer.ReadinessProbe.HTTPGet.Port.IntVal != int32(iv.AdminPort) {
 			return fmt.Errorf("readinessProbe: expected: %d, actual %d", iv.AdminPort, proxyContainer.LivenessProbe.HTTPGet.Port.IntVal)
 		}
-		if err := iv.validatePort(proxyContainer, "linkerd-proxy", iv.InboundPort); err != nil {
+		if err := iv.validatePort(proxyContainer, k8s.ProxyPortName, iv.InboundPort); err != nil {
 			return err
 		}
 	}
@@ -285,9 +282,9 @@ func (iv *InjectValidator) validateInitContainer(pod *v1.PodSpec) error {
 	if iv.NoInitContainer {
 		return nil
 	}
-	initContainer := iv.getContainer(pod, initContainerName, true)
+	initContainer := iv.getContainer(pod, k8s.InitContainerName, true)
 	if initContainer == nil {
-		return fmt.Errorf("container %s missing", initContainerName)
+		return fmt.Errorf("container %s missing", k8s.InitContainerName)
 	}
 
 	if iv.InitImage != "" || iv.InitImageVersion != "" {