Repository: linkerd/linkerd2
Pull request #12625 (2024-05-20 13:38:48)

Refactor ES addition logic in Destination
This is a second take on #12427, which avoided a theoretical/correctness issue around overwritting new ES addresses with stale data.

We had to revert that in #12589 because the change introduced a bug, by returning early when the ES had no addresses and failed to properly initiallize `addesses` for the portPublisher.

This just removes the early return.


Patch:
diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml
index dea8269d5..f7b742b60 100644
--- a/.github/workflows/actions.yml
+++ b/.github/workflows/actions.yml
@@ -15,12 +15,12 @@ jobs:
     timeout-minutes: 10
     steps:
       - uses: linkerd/dev/actions/setup-tools@v43
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: just-dev lint-actions
 
   devcontainer-versions:
     runs-on: ubuntu-22.04
     steps:
       - uses: linkerd/dev/actions/setup-tools@v43
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: just-dev check-action-images
diff --git a/.github/workflows/choco.yml b/.github/workflows/choco.yml
index 98bdf7f9a..cd76015f8 100644
--- a/.github/workflows/choco.yml
+++ b/.github/workflows/choco.yml
@@ -13,7 +13,7 @@ jobs:
     timeout-minutes: 10
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: devblackops/github-action-psscriptanalyzer@854038567344559afaaa8ccb7a014452b99d86ee
         env:
           # https://github.com/devblackops/github-action-psscriptanalyzer/pull/3/files
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
index 45d52ea82..e0b706f5f 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -16,10 +16,10 @@ jobs:
     container:
       image: golang:1.22
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: go install gotest.tools/gotestsum@v0.4.2
       - run: gotestsum -- -cover -coverprofile=coverage.out -v -mod=readonly ./...
-      - uses: codecov/codecov-action@6d798873df2b1b8e5846dba6fb86631229fbcb17
+      - uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c
         with:
           files: ./coverage.out
           flags: unittests,golang
@@ -31,7 +31,7 @@ jobs:
     container:
       image: node:20-stretch
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - name: Yarn setup
         shell: bash
         run: bin/scurl -o- https://yarnpkg.com/install.sh | bash -s -- --version 1.21.1 --network-concurrency 1
@@ -41,7 +41,7 @@ jobs:
           export NODE_ENV=test
           bin/web --frozen-lockfile
           bin/web test --reporters="jest-progress-bar-reporter" --reporters="./gh_ann_reporter.js" --coverage
-      - uses: codecov/codecov-action@6d798873df2b1b8e5846dba6fb86631229fbcb17
+      - uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c
         with:
           directory: ./web/app/coverage
           flags: unittests,javascript
@@ -54,10 +54,10 @@ jobs:
       image: docker://rust:1.76.0
       options: --security-opt seccomp=unconfined
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - shell: bash
         run: mkdir -p target && cd target && bin/scurl -v https://github.com/xd009642/tarpaulin/releases/download/0.27.3/cargo-tarpaulin-x86_64-unknown-linux-musl.tar.gz | tar zxvf - && chmod 755 cargo-tarpaulin
       - run: target/cargo-tarpaulin tarpaulin --workspace --out Xml
-      - uses: codecov/codecov-action@6d798873df2b1b8e5846dba6fb86631229fbcb17
+      - uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c
         with:
           flags: unittests,rust
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index f4278b271..09dce0026 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -40,7 +40,7 @@ jobs:
         with:
           go-version: "1.22"
       - name: Checkout
-        uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+        uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
 
       - name: Initialize
         # Unpinned action version so that we automatically get analyzer updates.
diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml
index 811e49469..6756f883f 100644
--- a/.github/workflows/devcontainer.yml
+++ b/.github/workflows/devcontainer.yml
@@ -17,7 +17,7 @@ jobs:
     runs-on: ubuntu-22.04
     container: ghcr.io/linkerd/dev:v43-rust
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - shell: bash
         run: |
           # Extract current Rust version from the toolchain file.
@@ -40,5 +40,5 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - uses: linkerd/dev/actions/setup-tools@v43
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: just-dev pull-dev-image
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 9c2f4ef59..3727236aa 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -8,8 +8,8 @@ jobs:
   meta:
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
-      - uses: tj-actions/changed-files@a29e8b565651ce417abb5db7164b4a2ad8b6155c
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
+      - uses: tj-actions/changed-files@1754cd4b9e661d1f0eced3b33545a8d8b3bc46d8
         id: changed
         with:
           files: |
@@ -31,7 +31,7 @@ jobs:
     runs-on: ubuntu-22.04
     container: ghcr.io/linkerd/dev:v43-go
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: git config --global --add safe.directory "$PWD" # actions/runner#2033
       - run: just go-lint --verbose --timeout=10m
 
@@ -42,7 +42,7 @@ jobs:
     runs-on: ubuntu-22.04
     container: ghcr.io/linkerd/dev:v43-go
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: git config --global --add safe.directory "$PWD" # actions/runner#2033
       - run: just go-fmt
 
@@ -53,7 +53,7 @@ jobs:
     runs-on: ubuntu-22.04
     container: ghcr.io/linkerd/dev:v43-go
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: git config --global --add safe.directory "$PWD" # actions/runner#2033
       - run: just go-fetch
       - run: just go-test
diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml
index 50272b6e2..2c05000ef 100644
--- a/.github/workflows/helm.yml
+++ b/.github/workflows/helm.yml
@@ -16,6 +16,6 @@ jobs:
     timeout-minutes: 5
     steps:
       - uses: linkerd/dev/actions/setup-tools@v43
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: helm-docs
       - run: git diff --exit-code -- **/charts/**/README.md
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index a178cf5fd..293fcbaf7 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -25,10 +25,10 @@ jobs:
   meta:
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - id: tag
         run: echo "tag=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> "$GITHUB_OUTPUT"
-      - uses: tj-actions/changed-files@a29e8b565651ce417abb5db7164b4a2ad8b6155c
+      - uses: tj-actions/changed-files@1754cd4b9e661d1f0eced3b33545a8d8b3bc46d8
         id: core
         with:
           files: |
@@ -65,7 +65,7 @@ jobs:
     runs-on: ubuntu-22.04
     timeout-minutes: 15
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: ./.github/actions/docker-build
         id: build
         with:
@@ -103,7 +103,7 @@ jobs:
           - proxy
     timeout-minutes: 20
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: ./.github/actions/docker-build
         id: build
         env:
@@ -134,7 +134,7 @@ jobs:
     runs-on: ubuntu-22.04
     timeout-minutes: 15
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7
         with:
           go-version: "1.22"
@@ -169,7 +169,7 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
       - uses: olix0r/cargo-action-fmt/setup@9269f3aa1ff01775d95efc97037e2cbdb41d9684
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e
         with:
           pattern: image-archives-*
@@ -227,7 +227,7 @@ jobs:
           - web
     timeout-minutes: 15
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: ./.github/actions/docker-build
         id: build
         with:
@@ -261,7 +261,7 @@ jobs:
     runs-on: ubuntu-22.04
     timeout-minutes: 15
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7
         with:
           go-version: "1.22"
@@ -284,9 +284,9 @@ jobs:
     needs: [meta, build-cli, build-core, build-ext]
     if: needs.meta.outputs.changed == 'true'
     runs-on: ubuntu-22.04
-    timeout-minutes: 15
+    timeout-minutes: 30
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7
         with:
           go-version: "1.22"
@@ -320,7 +320,7 @@ jobs:
       - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7
         with:
           go-version: "1.22"
diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml
index 502282a2a..2b6099817 100644
--- a/.github/workflows/js.yml
+++ b/.github/workflows/js.yml
@@ -19,7 +19,7 @@ jobs:
     env:
       NODE_ENV: test
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - name: Yarn setup
         shell: bash
         run: |
diff --git a/.github/workflows/markdown.yml b/.github/workflows/markdown.yml
index 3d16d1d6e..e4028d16e 100644
--- a/.github/workflows/markdown.yml
+++ b/.github/workflows/markdown.yml
@@ -14,7 +14,7 @@ jobs:
     timeout-minutes: 5
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: DavidAnson/markdownlint-cli2-action@b4c9feab76d8025d1e83c653fa3990936df0e6c8
         with:
           globs: |
diff --git a/.github/workflows/proto.yml b/.github/workflows/proto.yml
index 0231aa363..7105d7984 100644
--- a/.github/workflows/proto.yml
+++ b/.github/workflows/proto.yml
@@ -18,6 +18,6 @@ jobs:
     container: ghcr.io/linkerd/dev:v43-go
     steps:
       - run: apt update && apt install -y unzip
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: git config --global --add safe.directory "$PWD" # actions/runner#2033
       - run: bin/protoc-diff
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cf52ac05b..b74ffcd9f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -21,7 +21,7 @@ jobs:
   tag:
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: echo "tag=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> "$GITHUB_OUTPUT"
         id: tag
     outputs:
@@ -48,7 +48,7 @@ jobs:
     # policy-controller docker builds have occasionally hit a 30-minute timeout.
     timeout-minutes: 45
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - name: Set tag
         run: echo 'TAG=${{ needs.tag.outputs.tag }}' >> "$GITHUB_ENV"
       - uses: ./.github/actions/docker-build
@@ -95,7 +95,7 @@ jobs:
     needs: [docker_build]
     steps:
       - name: Checkout code
-        uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+        uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7
         with:
           go-version: "1.22"
@@ -127,7 +127,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: Checkout code
-        uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+        uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7
         with:
           go-version: "1.22"
@@ -156,7 +156,7 @@ jobs:
     steps:
       - name: Checkout code
         if: startsWith(github.ref, 'refs/tags/stable')
-        uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+        uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - name: Chocolatey - update nuspec
         if: startsWith(github.ref, 'refs/tags/stable')
         run: |
@@ -186,7 +186,7 @@ jobs:
     permissions:
       contents: write
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       # - name: Download choco package
       #   if: startsWith(github.ref, 'refs/tags/stable')
       #   uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e
@@ -234,7 +234,7 @@ jobs:
     if: startsWith(github.ref, 'refs/tags/stable') || startsWith(github.ref, 'refs/tags/edge')
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - name: Set install target for stable
         if: startsWith(github.ref, 'refs/tags/stable')
         run: echo "INSTALL=install" >> "$GITHUB_ENV"
@@ -262,7 +262,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: Checkout code
-        uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+        uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - name: Log into GCP
         uses: "google-github-actions/auth@71fee32a0bb7e97b4d33d548e7d957010649d8fa"
         with:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index ef2c9351e..2aa72b111 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -35,7 +35,7 @@ jobs:
     # Prevent sudden announcement of a new advisory from failing Ci.
     continue-on-error: ${{ matrix.checks == 'advisories' }}
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - uses: EmbarkStudios/cargo-deny-action@3f4a782664881cf5725d0ffd23969fcce89fd868
         with:
           command: check ${{ matrix.checks }}
@@ -45,7 +45,7 @@ jobs:
     runs-on: ubuntu-22.04
     container: ghcr.io/linkerd/dev:v43-rust
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: git config --global --add safe.directory "$PWD" # actions/runner#2033
       - run: just rs-check-fmt
 
@@ -54,7 +54,7 @@ jobs:
     runs-on: ubuntu-22.04
     container: ghcr.io/linkerd/dev:v43-rust
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: git config --global --add safe.directory "$PWD" # actions/runner#2033
       - run: just rs-fetch
       - run: just rs-clippy
@@ -65,7 +65,7 @@ jobs:
     runs-on: ubuntu-22.04
     container: ghcr.io/linkerd/dev:v43-rust
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: git config --global --add safe.directory "$PWD" # actions/runner#2033
       - run: just rs-fetch
       - run: just rs-check-dirs
@@ -76,7 +76,7 @@ jobs:
     timeout-minutes: 15
     container: ghcr.io/linkerd/dev:v43-rust
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: git config --global --add safe.directory "$PWD" # actions/runner#2033
       - run: just rs-fetch
       - run: just rs-test-build
@@ -87,7 +87,7 @@ jobs:
     runs-on: ubuntu-22.04
     timeout-minutes: 2
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - shell: bash
         run: |
           toolchain_version="$(./bin/rust-toolchain-version)"
diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml
index 3bc52dae4..ea383514f 100644
--- a/.github/workflows/shell.yml
+++ b/.github/workflows/shell.yml
@@ -17,5 +17,5 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - uses: linkerd/dev/actions/setup-tools@v43
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
       - run: just sh-lint
diff --git a/.github/workflows/sync-proxy.yml b/.github/workflows/sync-proxy.yml
index bc7ac8f4c..f1fd907f1 100644
--- a/.github/workflows/sync-proxy.yml
+++ b/.github/workflows/sync-proxy.yml
@@ -64,7 +64,7 @@ jobs:
     env:
       VERSION: ${{ needs.meta.outputs.name }}
     steps:
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
         with:
           token: ${{ secrets.LINKERD2_PROXY_GITHUB_TOKEN || github.token }}
       - name: Check if proxy version has changed
@@ -98,7 +98,7 @@ jobs:
           git config --global --add safe.directory "$PWD" # actions/runner#2033
           git config --global user.name "$GITHUB_USERNAME"
           git config --global user.email "$GITHUB_USERNAME"@users.noreply.github.com
-      - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b
+      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
         with:
           token: ${{ secrets.LINKERD2_PROXY_GITHUB_TOKEN || github.token }}
       - name: Commit proxy version
diff --git a/Cargo.lock b/Cargo.lock
index 22ab5bc0e..b19475bee 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -74,9 +74,9 @@ checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
 
 [[package]]
 name = "anyhow"
-version = "1.0.83"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
 
 [[package]]
 name = "async-stream"
@@ -244,9 +244,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
 
 [[package]]
 name = "cc"
-version = "1.0.97"
+version = "1.0.98"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
+checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
 
 [[package]]
 name = "cfg-if"
@@ -332,9 +332,9 @@ dependencies = [
 
 [[package]]
 name = "crc32fast"
-version = "1.4.0"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
 dependencies = [
  "cfg-if",
 ]
@@ -444,9 +444,9 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
 
 [[package]]
 name = "either"
-version = "1.11.0"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
+checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
 
 [[package]]
 name = "equivalent"
@@ -852,9 +852,9 @@ dependencies = [
 
 [[package]]
 name = "instant"
-version = "0.1.12"
+version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
 dependencies = [
  "cfg-if",
 ]
@@ -1338,9 +1338,9 @@ dependencies = [
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.13"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
 [[package]]
 name = "lock_api"
@@ -1654,9 +1654,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.82"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
+checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43"
 dependencies = [
  "unicode-ident",
 ]
@@ -1709,9 +1709,9 @@ dependencies = [
 
 [[package]]
 name = "prost"
-version = "0.12.4"
+version = "0.12.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922"
+checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29"
 dependencies = [
  "bytes",
  "prost-derive",
@@ -1719,9 +1719,9 @@ dependencies = [
 
 [[package]]
 name = "prost-derive"
-version = "0.12.4"
+version = "0.12.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48"
+checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
 dependencies = [
  "anyhow",
  "itertools",
@@ -1732,9 +1732,9 @@ dependencies = [
 
 [[package]]
 name = "prost-types"
-version = "0.12.4"
+version = "0.12.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe"
+checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0"
 dependencies = [
  "prost",
 ]
@@ -1931,9 +1931,9 @@ dependencies = [
 
 [[package]]
 name = "schemars"
-version = "0.8.19"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef"
+checksum = "b0218ceea14babe24a4a5836f86ade86c1effbc198164e619194cb5069187e29"
 dependencies = [
  "dyn-clone",
  "schemars_derive",
@@ -1943,9 +1943,9 @@ dependencies = [
 
 [[package]]
 name = "schemars_derive"
-version = "0.8.19"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49"
+checksum = "3ed5a1ccce8ff962e31a165d41f6e2a2dd1245099dc4d594f5574a86cd90f4d3"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/cli/cmd/inject.go b/cli/cmd/inject.go
index 117f65f9f..e671bcbfe 100644
--- a/cli/cmd/inject.go
+++ b/cli/cmd/inject.go
@@ -42,8 +42,8 @@ type resourceTransformerInject struct {
 	closeWaitTimeout    time.Duration
 }
 
-func runInjectCmd(inputs []io.Reader, errWriter, outWriter io.Writer, transformer *resourceTransformerInject) int {
-	return transformInput(inputs, errWriter, outWriter, transformer)
+func runInjectCmd(inputs []io.Reader, errWriter, outWriter io.Writer, transformer *resourceTransformerInject, output string) int {
+	return transformInput(inputs, errWriter, outWriter, transformer, output)
 }
 
 func newCmdInject() *cobra.Command {
@@ -56,6 +56,7 @@ func newCmdInject() *cobra.Command {
 	injectFlags, injectFlagSet := makeInjectFlags(defaults)
 	var manualOption, enableDebugSidecar bool
 	var closeWaitTimeout time.Duration
+	var output string
 
 	cmd := &cobra.Command{
 		Use:   "inject [flags] CONFIG-FILE",
@@ -109,7 +110,7 @@ sub-folders, or coming from stdin.`,
 				enableDebugSidecar:  enableDebugSidecar,
 				closeWaitTimeout:    closeWaitTimeout,
 			}
-			exitCode := uninjectAndInject(in, stderr, stdout, transformer)
+			exitCode := uninjectAndInject(in, stderr, stdout, transformer, output)
 			os.Exit(exitCode)
 			return nil
 		},
@@ -132,18 +133,20 @@ sub-folders, or coming from stdin.`,
 		&closeWaitTimeout, "close-wait-timeout", closeWaitTimeout,
 		"Sets nf_conntrack_tcp_timeout_close_wait")
 
+	cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format, one of: json|yaml")
+
 	cmd.Flags().AddFlagSet(proxyFlagSet)
 	cmd.Flags().AddFlagSet(injectFlagSet)
 
 	return cmd
 }
 
-func uninjectAndInject(inputs []io.Reader, errWriter, outWriter io.Writer, transformer *resourceTransformerInject) int {
+func uninjectAndInject(inputs []io.Reader, errWriter, outWriter io.Writer, transformer *resourceTransformerInject, output string) int {
 	var out bytes.Buffer
-	if exitCode := runUninjectSilentCmd(inputs, errWriter, &out, transformer.values); exitCode != 0 {
+	if exitCode := runUninjectSilentCmd(inputs, errWriter, &out, transformer.values, "yaml"); exitCode != 0 {
 		return exitCode
 	}
-	return runInjectCmd([]io.Reader{&out}, errWriter, outWriter, transformer)
+	return runInjectCmd([]io.Reader{&out}, errWriter, outWriter, transformer, output)
 }
 
 func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Report, error) {
diff --git a/cli/cmd/inject_util.go b/cli/cmd/inject_util.go
index ecca891cc..a377d493f 100644
--- a/cli/cmd/inject_util.go
+++ b/cli/cmd/inject_util.go
@@ -26,12 +26,12 @@ type resourceTransformer interface {
 }
 
 // Returns the integer representation of os.Exit code; 0 on success and 1 on failure.
-func transformInput(inputs []io.Reader, errWriter, outWriter io.Writer, rt resourceTransformer) int {
+func transformInput(inputs []io.Reader, errWriter, outWriter io.Writer, rt resourceTransformer, format string) int {
 	postInjectBuf := &bytes.Buffer{}
 	reportBuf := &bytes.Buffer{}
 
 	for _, input := range inputs {
-		errs := processYAML(input, postInjectBuf, reportBuf, rt)
+		errs := processYAML(input, postInjectBuf, reportBuf, rt, format)
 		if len(errs) > 0 {
 			fmt.Fprintf(errWriter, "Error transforming resources:\n%v", concatErrors(errs, "\n"))
 			return 1
@@ -51,7 +51,7 @@ func transformInput(inputs []io.Reader, errWriter, outWriter io.Writer, rt resou
 }
 
 // processYAML takes an input stream of YAML, outputting injected/uninjected YAML to out.
-func processYAML(in io.Reader, out io.Writer, report io.Writer, rt resourceTransformer) []error {
+func processYAML(in io.Reader, out io.Writer, report io.Writer, rt resourceTransformer, format string) []error {
 	reader := yamlDecoder.NewYAMLReader(bufio.NewReaderSize(in, 4096))
 
 	reports := []inject.Report{}
@@ -86,9 +86,26 @@ func processYAML(in io.Reader, out io.Writer, report io.Writer, rt resourceTrans
 		}
 		reports = append(reports, irs...)
 
+		// If the format is set to json, we need to convert the yaml to json
+		if format == "json" {
+			result, err = yaml.YAMLToJSON(result)
+			if err != nil {
+				errs = append(errs, err)
+			}
+		} else if format == "yaml" {
+			// result is already in yaml format: noop.
+		} else {
+			errs = append(errs, fmt.Errorf("unsupported format %s", format))
+		}
+
 		if len(errs) == 0 {
 			out.Write(result)
-			out.Write([]byte("---\n"))
+			if format == "yaml" {
+				out.Write([]byte("---\n"))
+			}
+			if format == "json" {
+				out.Write([]byte("\n"))
+			}
 		}
 	}
 
diff --git a/cli/cmd/policy.go b/cli/cmd/policy.go
index f6478bdfb..b96174c98 100644
--- a/cli/cmd/policy.go
+++ b/cli/cmd/policy.go
@@ -132,7 +132,7 @@ displayed.`,
 				client := outbound.NewOutboundPoliciesClient(conn)
 
 				result, err = client.Get(cmd.Context(), &outbound.TrafficSpec{
-					SourceWorkload: "default:diagnostics",
+					SourceWorkload: options.contextToken,
 					Target:         &outbound.TrafficSpec_Authority{Authority: fmt.Sprintf("%s.%s.svc:%d", name, namespace, port)},
 				})
 				if err != nil {
@@ -166,6 +166,7 @@ displayed.`,
 	}
 
 	cmd.PersistentFlags().StringVar(&options.destinationPod, "destination-pod", "", "Target a specific destination Pod when there are multiple running")
+	cmd.PersistentFlags().StringVar(&options.contextToken, "token", "default:diagnostics", "Token to use when querying the policy service")
 	cmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", namespace, "Namespace of resource")
 	cmd.PersistentFlags().StringVarP(&output, "output", "o", output, "Output format. One of: yaml, json")
 
diff --git a/cli/cmd/profile.go b/cli/cmd/profile.go
index 58aa192f5..9cc796a88 100644
--- a/cli/cmd/profile.go
+++ b/cli/cmd/profile.go
@@ -1,16 +1,20 @@
 package cmd
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
 	"os"
 
+	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/profiles"
 	"github.com/spf13/cobra"
 	"k8s.io/apimachinery/pkg/util/validation"
+	"sigs.k8s.io/yaml"
 )
 
 type profileOptions struct {
@@ -20,6 +24,7 @@ type profileOptions struct {
 	openAPI       string
 	proto         string
 	ignoreCluster bool
+	output        string
 }
 
 func newProfileOptions() *profileOptions {
@@ -29,6 +34,7 @@ func newProfileOptions() *profileOptions {
 		openAPI:       "",
 		proto:         "",
 		ignoreCluster: false,
+		output:        "yaml",
 	}
 }
 
@@ -112,16 +118,21 @@ func newCmdProfile() *cobra.Command {
 				}
 			}
 
+			var profile *sp.ServiceProfile
 			if options.template {
-				return profiles.RenderProfileTemplate(options.namespace, options.name, clusterDomain, os.Stdout)
+				return profiles.RenderProfileTemplate(options.namespace, options.name, clusterDomain, os.Stdout, options.output)
 			} else if options.openAPI != "" {
-				return profiles.RenderOpenAPI(options.openAPI, options.namespace, options.name, clusterDomain, os.Stdout)
+				profile, err = profiles.RenderOpenAPI(options.openAPI, options.namespace, options.name, clusterDomain)
 			} else if options.proto != "" {
-				return profiles.RenderProto(options.proto, options.namespace, options.name, clusterDomain, os.Stdout)
+				profile, err = profiles.RenderProto(options.proto, options.namespace, options.name, clusterDomain)
+			} else {
+				return errors.New("one of --template, --open-api, or --proto must be specified")
+			}
+			if err != nil {
+				return err
 			}
 
-			// we should never get here
-			return errors.New("Unexpected error")
+			return writeProfile(profile, os.Stdout, options.output)
 		},
 	}
 
@@ -130,6 +141,23 @@ func newCmdProfile() *cobra.Command {
 	cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the service")
 	cmd.PersistentFlags().StringVar(&options.proto, "proto", options.proto, "Output a service profile based on the given Protobuf spec file")
 	cmd.PersistentFlags().BoolVar(&options.ignoreCluster, "ignore-cluster", options.ignoreCluster, "Output a service profile through offline generation")
-
+	cmd.PersistentFlags().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: yaml, json")
 	return cmd
 }
+
+func writeProfile(profile *sp.ServiceProfile, w io.Writer, format string) error {
+	var output []byte
+	var err error
+	if format == "yaml" {
+		output, err = yaml.Marshal(profile)
+	} else if format == "json" {
+		output, err = json.Marshal(profile)
+	} else {
+		return fmt.Errorf("unknown output format: %s", format)
+	}
+	if err != nil {
+		return fmt.Errorf("Error writing Service Profile: %w", err)
+	}
+	_, err = w.Write(output)
+	return err
+}
diff --git a/cli/cmd/uninject.go b/cli/cmd/uninject.go
index e608ff81a..88299540a 100644
--- a/cli/cmd/uninject.go
+++ b/cli/cmd/uninject.go
@@ -18,15 +18,17 @@ type resourceTransformerUninjectSilent struct {
 	values *linkerd2.Values
 }
 
-func runUninjectCmd(inputs []io.Reader, errWriter, outWriter io.Writer, values *linkerd2.Values) int {
-	return transformInput(inputs, errWriter, outWriter, resourceTransformerUninject{values})
+func runUninjectCmd(inputs []io.Reader, errWriter, outWriter io.Writer, values *linkerd2.Values, output string) int {
+	return transformInput(inputs, errWriter, outWriter, resourceTransformerUninject{values}, output)
 }
 
-func runUninjectSilentCmd(inputs []io.Reader, errWriter, outWriter io.Writer, values *linkerd2.Values) int {
-	return transformInput(inputs, errWriter, outWriter, resourceTransformerUninjectSilent{values})
+func runUninjectSilentCmd(inputs []io.Reader, errWriter, outWriter io.Writer, values *linkerd2.Values, output string) int {
+	return transformInput(inputs, errWriter, outWriter, resourceTransformerUninjectSilent{values}, output)
 }
 
 func newCmdUninject() *cobra.Command {
+	var output string
+
 	cmd := &cobra.Command{
 		Use:   "uninject [flags] CONFIG-FILE",
 		Short: "Remove the Linkerd proxy from a Kubernetes config",
@@ -53,12 +55,14 @@ sub-folders, or coming from stdin.`,
 				return err
 			}
 
-			exitCode := runUninjectCmd(in, os.Stderr, os.Stdout, nil)
+			exitCode := runUninjectCmd(in, os.Stderr, os.Stdout, nil, output)
 			os.Exit(exitCode)
 			return nil
 		},
 	}
 
+	cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format, one of: json|yaml")
+
 	return cmd
 }
 
diff --git a/controller/api/destination/watcher/endpoints_watcher.go b/controller/api/destination/watcher/endpoints_watcher.go
index b4e9dba00..6bc0be848 100644
--- a/controller/api/destination/watcher/endpoints_watcher.go
+++ b/controller/api/destination/watcher/endpoints_watcher.go
@@ -795,8 +795,9 @@ func (pp *portPublisher) updateEndpoints(endpoints *corev1.Endpoints) {
 func (pp *portPublisher) addEndpointSlice(slice *discovery.EndpointSlice) {
 	newAddressSet := pp.endpointSliceToAddresses(slice)
 	for id, addr := range pp.addresses.Addresses {
-		newAddressSet.Addresses[id] = addr
-		newAddressSet.LocalTrafficPolicy = pp.localTrafficPolicy
+		if _, ok := newAddressSet.Addresses[id]; !ok {
+			newAddressSet.Addresses[id] = addr
+		}
 	}
 
 	add, _ := diffAddresses(pp.addresses, newAddressSet)
@@ -806,6 +807,11 @@ func (pp *portPublisher) addEndpointSlice(slice *discovery.EndpointSlice) {
 		}
 	}
 
+	// even if the ES doesn't have addresses yet we need to create a new
+	// pp.addresses entry with the appropriate Labels and LocalTrafficPolicy,
+	// which isn't going to be captured during the ES update event when
+	// addresses get added
+
 	pp.addresses = newAddressSet
 	pp.exists = true
 	pp.metrics.incUpdates()
diff --git a/go.mod b/go.mod
index 2eb8ac0f8..e9cc1bb9d 100644
--- a/go.mod
+++ b/go.mod
@@ -49,7 +49,7 @@ require (
 	k8s.io/code-generator v0.30.1
 	k8s.io/endpointslice v0.30.1
 	k8s.io/klog/v2 v2.120.1
-	k8s.io/kube-aggregator v0.30.0
+	k8s.io/kube-aggregator v0.30.1
 	k8s.io/utils v0.0.0-20230726121419-3b25d923346b
 	sigs.k8s.io/gateway-api v0.8.1
 	sigs.k8s.io/yaml v1.4.0
diff --git a/go.sum b/go.sum
index 52e952821..a02147010 100644
--- a/go.sum
+++ b/go.sum
@@ -774,8 +774,8 @@ k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1
 k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8=
 k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
 k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/kube-aggregator v0.30.0 h1:+Opc0lmhRmHbNM4m3mLSsUFmK/ikMapO9rvGirX5CEM=
-k8s.io/kube-aggregator v0.30.0/go.mod h1:KbZZkSSjYE6vkB2TSuZ9GBjU3ucgL7YxT8yX8wll0iQ=
+k8s.io/kube-aggregator v0.30.1 h1:ymR2BsxDacTKwzKTuNhGZttuk009c+oZbSeD+IPX5q4=
+k8s.io/kube-aggregator v0.30.1/go.mod h1:SFbqWsM6ea8dHd3mPLsZFzJHbjBOS5ykIgJh4znZ5iQ=
 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
 k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
diff --git a/pkg/profiles/openapi.go b/pkg/profiles/openapi.go
index dc0f9f1c6..58c7fde51 100644
--- a/pkg/profiles/openapi.go
+++ b/pkg/profiles/openapi.go
@@ -21,31 +21,31 @@ const (
 // RenderOpenAPI reads an OpenAPI spec file and renders the corresponding
 // ServiceProfile to a buffer, given a namespace, service, and control plane
 // namespace.
-func RenderOpenAPI(fileName, namespace, name, clusterDomain string, w io.Writer) error {
+func RenderOpenAPI(fileName, namespace, name, clusterDomain string) (*sp.ServiceProfile, error) {
 
 	input, err := readFile(fileName)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	bytes, err := io.ReadAll(input)
 	if err != nil {
-		return fmt.Errorf("Error reading file: %w", err)
+		return nil, fmt.Errorf("Error reading file: %w", err)
 	}
 	json, err := yaml.YAMLToJSON(bytes)
 	if err != nil {
-		return fmt.Errorf("Error parsing yaml: %w", err)
+		return nil, fmt.Errorf("Error parsing yaml: %w", err)
 	}
 
 	swagger := spec.Swagger{}
 	err = swagger.UnmarshalJSON(json)
 	if err != nil {
-		return fmt.Errorf("Error parsing OpenAPI spec: %w", err)
+		return nil, fmt.Errorf("Error parsing OpenAPI spec: %w", err)
 	}
 
 	profile := swaggerToServiceProfile(swagger, namespace, name, clusterDomain)
 
-	return writeProfile(profile, w)
+	return &profile, nil
 }
 
 func swaggerToServiceProfile(swagger spec.Swagger, namespace, name, clusterDomain string) sp.ServiceProfile {
diff --git a/pkg/profiles/profiles.go b/pkg/profiles/profiles.go
index 994104d6f..fd4684b6d 100644
--- a/pkg/profiles/profiles.go
+++ b/pkg/profiles/profiles.go
@@ -15,6 +15,7 @@ import (
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/validation"
+	yamlDecoder "k8s.io/apimachinery/pkg/util/yaml"
 	"sigs.k8s.io/yaml"
 )
 
@@ -210,7 +211,7 @@ func buildConfig(namespace, service, clusterDomain string) *profileTemplateConfi
 
 // RenderProfileTemplate renders a ServiceProfile template to a buffer, given a
 // namespace, service, and control plane namespace.
-func RenderProfileTemplate(namespace, service, clusterDomain string, w io.Writer) error {
+func RenderProfileTemplate(namespace, service, clusterDomain string, w io.Writer, format string) error {
 	config := buildConfig(namespace, service, clusterDomain)
 	template, err := template.New("profile").Parse(Template)
 	if err != nil {
@@ -222,8 +223,20 @@ func RenderProfileTemplate(namespace, service, clusterDomain string, w io.Writer
 		return err
 	}
 
-	_, err = w.Write(buf.Bytes())
-	return err
+	if format == "json" {
+		bytes, err := yamlDecoder.ToJSON(buf.Bytes())
+		if err != nil {
+			return err
+		}
+		_, err = w.Write(append(bytes, '\n'))
+		return err
+	}
+	if format == "yaml" {
+		_, err = w.Write(buf.Bytes())
+		return err
+	}
+
+	return fmt.Errorf("unknown output format: %s", format)
 }
 
 func readFile(fileName string) (io.Reader, error) {
@@ -233,15 +246,6 @@ func readFile(fileName string) (io.Reader, error) {
 	return os.Open(filepath.Clean(fileName))
 }
 
-func writeProfile(profile sp.ServiceProfile, w io.Writer) error {
-	output, err := yaml.Marshal(profile)
-	if err != nil {
-		return fmt.Errorf("Error writing Service Profile: %w", err)
-	}
-	_, err = w.Write(output)
-	return err
-}
-
 // PathToRegex converts a path into a regex.
 func PathToRegex(path string) string {
 	escaped := regexp.QuoteMeta(path)
diff --git a/pkg/profiles/profiles_fuzzer.go b/pkg/profiles/profiles_fuzzer.go
index c2e38539e..1db029302 100644
--- a/pkg/profiles/profiles_fuzzer.go
+++ b/pkg/profiles/profiles_fuzzer.go
@@ -42,11 +42,9 @@ func FuzzRenderProto(data []byte) int {
 	if err != nil {
 		return 0
 	}
-	w, err := os.OpenFile("/dev/null", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
+	_, err = RenderProto(protofile.Name(), namespace, name, clusterDomain)
 	if err != nil {
 		return 0
 	}
-	defer w.Close()
-	_ = RenderProto(protofile.Name(), namespace, name, clusterDomain, w)
 	return 1
 }
diff --git a/pkg/profiles/proto.go b/pkg/profiles/proto.go
index 26c3d48ea..e86aa0a2a 100644
--- a/pkg/profiles/proto.go
+++ b/pkg/profiles/proto.go
@@ -2,7 +2,6 @@ package profiles
 
 import (
 	"fmt"
-	"io"
 	"net/http"
 	"regexp"
 	"strings"
@@ -15,20 +14,15 @@ import (
 // RenderProto reads a protobuf definition file and renders the corresponding
 // ServiceProfile to a buffer, given a namespace, service, and control plane
 // namespace.
-func RenderProto(fileName, namespace, name, clusterDomain string, w io.Writer) error {
+func RenderProto(fileName, namespace, name, clusterDomain string) (*sp.ServiceProfile, error) {
 	input, err := readFile(fileName)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	parser := proto.NewParser(input)
 
-	profile, err := protoToServiceProfile(parser, namespace, name, clusterDomain)
-	if err != nil {
-		return err
-	}
-
-	return writeProfile(*profile, w)
+	return protoToServiceProfile(parser, namespace, name, clusterDomain)
 }
 
 func protoToServiceProfile(parser *proto.Parser, namespace, name, clusterDomain string) (*sp.ServiceProfile, error) {
diff --git a/policy-controller/grpc/Cargo.toml b/policy-controller/grpc/Cargo.toml
index e1146f2e1..edc30ded5 100644
--- a/policy-controller/grpc/Cargo.toml
+++ b/policy-controller/grpc/Cargo.toml
@@ -14,7 +14,7 @@ hyper = { version = "0.14", features = ["http2", "server", "tcp"] }
 futures = { version = "0.3", default-features = false }
 linkerd-policy-controller-core = { path = "../core" }
 maplit = "1"
-prost-types = "0.12.4"
+prost-types = "0.12.6"
 tokio = { version = "1", features = ["macros"] }
 tonic = { version = "0.10", default-features = false }
 tracing = "0.1"
diff --git a/viz/cmd/profile.go b/viz/cmd/profile.go
index ed33fb5ce..ba7016c61 100644
--- a/viz/cmd/profile.go
+++ b/viz/cmd/profile.go
@@ -3,6 +3,7 @@ package cmd
 import (
 	"bufio"
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
@@ -36,12 +37,14 @@ type profileOptions struct {
 	tap           string
 	tapDuration   time.Duration
 	tapRouteLimit uint
+	output        string
 }
 
 func newProfileOptions() *profileOptions {
 	return &profileOptions{
 		tapDuration:   5 * time.Second,
 		tapRouteLimit: 20,
+		output:        "yaml",
 	}
 }
 
@@ -136,13 +139,14 @@ func newCmdProfile() *cobra.Command {
 			if cd := values.ClusterDomain; cd != "" {
 				clusterDomain = cd
 			}
-			return renderTapOutputProfile(cmd.Context(), k8sAPI, options.tap, options.namespace, options.name, clusterDomain, options.tapDuration, int(options.tapRouteLimit), os.Stdout)
+			return renderTapOutputProfile(cmd.Context(), k8sAPI, options.tap, options.namespace, options.name, clusterDomain, options.tapDuration, int(options.tapRouteLimit), options.output, os.Stdout)
 		},
 	}
 	cmd.PersistentFlags().StringVar(&options.tap, "tap", options.tap, "Output a service profile based on tap data for the given target resource")
 	cmd.PersistentFlags().DurationVar(&options.tapDuration, "tap-duration", options.tapDuration, "Duration over which tap data is collected (for example: \"10s\", \"1m\", \"10m\")")
 	cmd.PersistentFlags().UintVar(&options.tapRouteLimit, "tap-route-limit", options.tapRouteLimit, "Max number of routes to add to the profile")
 	cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the service")
+	cmd.PersistentFlags().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: yaml, json")
 
 	pkgcmd.ConfigureNamespaceFlagCompletion(
 		cmd, []string{"namespace"},
@@ -153,7 +157,7 @@ func newCmdProfile() *cobra.Command {
 // renderTapOutputProfile performs a tap on the desired resource and generates
 // a service profile with routes pre-populated from the tap data
 // Only inbound tap traffic is considered.
-func renderTapOutputProfile(ctx context.Context, k8sAPI *k8s.KubernetesAPI, tapResource, namespace, name, clusterDomain string, tapDuration time.Duration, routeLimit int, w io.Writer) error {
+func renderTapOutputProfile(ctx context.Context, k8sAPI *k8s.KubernetesAPI, tapResource, namespace, name, clusterDomain string, tapDuration time.Duration, routeLimit int, format string, w io.Writer) error {
 	requestParams := pkg.TapRequestParams{
 		Resource:  tapResource,
 		Namespace: namespace,
@@ -167,7 +171,14 @@ func renderTapOutputProfile(ctx context.Context, k8sAPI *k8s.KubernetesAPI, tapR
 	if err != nil {
 		return err
 	}
-	output, err := yaml.Marshal(profile)
+	var output []byte
+	if format == "yaml" {
+		output, err = yaml.Marshal(profile)
+	} else if format == "json" {
+		output, err = json.Marshal(profile)
+	} else {
+		return errors.New("output format must be one of yaml or json")
+	}
 	if err != nil {
 		return fmt.Errorf("Error writing Service Profile: %w", err)
 	}
diff --git a/web/srv/handlers.go b/web/srv/handlers.go
index e3c9e84a3..e6ac5e7c5 100644
--- a/web/srv/handlers.go
+++ b/web/srv/handlers.go
@@ -74,7 +74,7 @@ func (h *handler) handleProfileDownload(w http.ResponseWriter, req *http.Request
 	}
 
 	profileYaml := &bytes.Buffer{}
-	err := profiles.RenderProfileTemplate(namespace, service, h.clusterDomain, profileYaml)
+	err := profiles.RenderProfileTemplate(namespace, service, h.clusterDomain, profileYaml, "yaml")
 
 	if err != nil {
 		log.Error(err)


Test patch:
diff --git a/cli/cmd/inject_test.go b/cli/cmd/inject_test.go
index 910b9eed6..7db7430b7 100644
--- a/cli/cmd/inject_test.go
+++ b/cli/cmd/inject_test.go
@@ -52,7 +52,7 @@ func testUninjectAndInject(t *testing.T, tc testCase) {
 		allowNsInject:       true,
 	}
 
-	if exitCode := uninjectAndInject([]io.Reader{read}, report, output, transformer); exitCode != 0 {
+	if exitCode := uninjectAndInject([]io.Reader{read}, report, output, transformer, "yaml"); exitCode != 0 {
 		t.Errorf("Unexpected error injecting YAML: %v", report)
 	}
 	if err := testDataDiffer.DiffTestYAML(tc.goldenFileName, output.String()); err != nil {
@@ -432,7 +432,7 @@ func testInjectCmd(t *testing.T, tc injectCmd) {
 		injectProxy: tc.injectProxy,
 		values:      testConfig,
 	}
-	exitCode := runInjectCmd([]io.Reader{in}, errBuffer, outBuffer, transformer)
+	exitCode := runInjectCmd([]io.Reader{in}, errBuffer, outBuffer, transformer, "yaml")
 	if exitCode != tc.exitCode {
 		t.Fatalf("Expected exit code to be %d but got: %d", tc.exitCode, exitCode)
 	}
@@ -538,7 +538,7 @@ func testInjectFilePath(t *testing.T, tc injectFilePath) {
 		injectProxy: true,
 		values:      values,
 	}
-	if exitCode := runInjectCmd(in, errBuf, actual, transformer); exitCode != 0 {
+	if exitCode := runInjectCmd(in, errBuf, actual, transformer, "yaml"); exitCode != 0 {
 		t.Fatal("Unexpected error. Exit code from runInjectCmd: ", exitCode)
 	}
 	if err := testDataDiffer.DiffTestYAML(tc.expectedFile, actual.String()); err != nil {
@@ -565,7 +565,7 @@ func testReadFromFolder(t *testing.T, resourceFolder string, expectedFolder stri
 		injectProxy: true,
 		values:      values,
 	}
-	if exitCode := runInjectCmd(in, errBuf, actual, transformer); exitCode != 0 {
+	if exitCode := runInjectCmd(in, errBuf, actual, transformer, "yaml"); exitCode != 0 {
 		t.Fatal("Unexpected error. Exit code from runInjectCmd: ", exitCode)
 	}
 
diff --git a/cli/cmd/profile_test.go b/cli/cmd/profile_test.go
index 7b4ae0c96..463f068ad 100644
--- a/cli/cmd/profile_test.go
+++ b/cli/cmd/profile_test.go
@@ -14,7 +14,7 @@ import (
 func TestParseProfile(t *testing.T) {
 	var buf bytes.Buffer
 
-	err := profiles.RenderProfileTemplate("myns", "mysvc", "mycluster.local", &buf)
+	err := profiles.RenderProfileTemplate("myns", "mysvc", "mycluster.local", &buf, "yaml")
 	if err != nil {
 		t.Fatalf("Error rendering service profile template: %v", err)
 	}
diff --git a/cli/cmd/uninject_test.go b/cli/cmd/uninject_test.go
index ba4d41f9f..96fbad7d8 100644
--- a/cli/cmd/uninject_test.go
+++ b/cli/cmd/uninject_test.go
@@ -116,7 +116,7 @@ func TestUninjectYAML(t *testing.T) {
 			output := new(bytes.Buffer)
 			report := new(bytes.Buffer)
 
-			exitCode := runUninjectCmd(read, report, output, values)
+			exitCode := runUninjectCmd(read, report, output, values, "yaml")
 			if exitCode != 0 {
 				t.Errorf("Failed to uninject %s", tc.inputFileName)
 			}