Repository: linkerd/linkerd2
Pull request #8560 (2022-05-26 03:42:51)

build(deps): bump github.com/imdario/mergo from 0.3.12 to 0.3.13
Bumps [github.com/imdario/mergo](https://github.com/imdario/mergo) from 0.3.12 to 0.3.13.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/imdario/mergo/releases">github.com/imdario/mergo's releases</a>.</em></p>
<blockquote>
<h2>Merging same-type slices supported, YAML upgraded to v3 (only used in tests), and more flexibility for transformers</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix typo in README.md by <a href="https://github.com/eltociear"><code>@​eltociear</code></a> in <a href="https://github-redirect.dependabot.com/imdario/mergo/pull/184">imdario/mergo#184</a></li>
<li>Adding StructBot to Mergo in the wild section by <a href="https://github.com/tjpnz"><code>@​tjpnz</code></a> in <a href="https://github-redirect.dependabot.com/imdario/mergo/pull/193">imdario/mergo#193</a></li>
<li>Add Goreleaser to featured projects by <a href="https://github.com/marcauberer"><code>@​marcauberer</code></a> in <a href="https://github-redirect.dependabot.com/imdario/mergo/pull/191">imdario/mergo#191</a></li>
<li>fix: gate transformers on valid non-nil destinations by <a href="https://github.com/zaquestion"><code>@​zaquestion</code></a> in <a href="https://github-redirect.dependabot.com/imdario/mergo/pull/211">imdario/mergo#211</a></li>
<li>Support slices of same type by <a href="https://github.com/heaths"><code>@​heaths</code></a> in <a href="https://github-redirect.dependabot.com/imdario/mergo/pull/210">imdario/mergo#210</a></li>
<li>Upgrade yaml to v3 by <a href="https://github.com/imdario"><code>@​imdario</code></a> in <a href="https://github-redirect.dependabot.com/imdario/mergo/pull/212">imdario/mergo#212</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/eltociear"><code>@​eltociear</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/imdario/mergo/pull/184">imdario/mergo#184</a></li>
<li><a href="https://github.com/tjpnz"><code>@​tjpnz</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/imdario/mergo/pull/193">imdario/mergo#193</a></li>
<li><a href="https://github.com/marcauberer"><code>@​marcauberer</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/imdario/mergo/pull/191">imdario/mergo#191</a></li>
<li><a href="https://github.com/zaquestion"><code>@​zaquestion</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/imdario/mergo/pull/211">imdario/mergo#211</a></li>
<li><a href="https://github.com/heaths"><code>@​heaths</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/imdario/mergo/pull/210">imdario/mergo#210</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/imdario/mergo/compare/0.3.12...v0.3.13">https://github.com/imdario/mergo/compare/0.3.12...v0.3.13</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/imdario/mergo/commit/194978757031c1a5c8ff8e059ae75fc2fc2a04f7"><code>1949787</code></a> Update README.md</li>
<li><a href="https://github.com/imdario/mergo/commit/80c13e16e74fb7da089e80fccdc1e5803ea8f346"><code>80c13e1</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/imdario/mergo/issues/212">#212</a> from imdario/dcc/issue-206</li>
<li><a href="https://github.com/imdario/mergo/commit/f86e6f60b76f0b19e53ff2fc409286667f7de9e9"><code>f86e6f6</code></a> Upgrade yaml to v3</li>
<li><a href="https://github.com/imdario/mergo/commit/39ebbbb298722dd51d14ae652ddca0f269a9afe5"><code>39ebbbb</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/imdario/mergo/issues/210">#210</a> from heaths/issue209</li>
<li><a href="https://github.com/imdario/mergo/commit/fd7d2bcdb08b91ac29e166e6056569c983347088"><code>fd7d2bc</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/imdario/mergo/issues/211">#211</a> from zaquestion/transformer_valid_destination</li>
<li><a href="https://github.com/imdario/mergo/commit/8109749b3ef7378913953135f986c307d5f159a9"><code>8109749</code></a> add test for deepMerge panic</li>
<li><a href="https://github.com/imdario/mergo/commit/4bed36ec55e461d22a3a5d17c1525693d9587cc8"><code>4bed36e</code></a> add test for keeping zero values with transformer</li>
<li><a href="https://github.com/imdario/mergo/commit/ab6b2707a132405e07fe89b72a9f70ee927dec56"><code>ab6b270</code></a> fix: gate transformers on valid non-nil destinations</li>
<li><a href="https://github.com/imdario/mergo/commit/200f90d97053f846f603a88fa283dccc30198142"><code>200f90d</code></a> Support slices of same type</li>
<li><a href="https://github.com/imdario/mergo/commit/fd3dfc91a2151957f2fe870772747a469bc89ced"><code>fd3dfc9</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/imdario/mergo/issues/191">#191</a> from marcauberer/master</li>
<li>Additional commits viewable in <a href="https://github.com/imdario/mergo/compare/0.3.12...v0.3.13">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/imdario/mergo&package-manager=go_modules&previous-version=0.3.12&new-version=0.3.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/policy_controller.yml b/.github/workflows/policy_controller.yml
index 5fdf33a3b..ccec505f9 100644
--- a/.github/workflows/policy_controller.yml
+++ b/.github/workflows/policy_controller.yml
@@ -27,105 +27,6 @@ env:
 
 
 jobs:
-  fmt:
-    timeout-minutes: 5
-    runs-on: ubuntu-latest
-    container:
-      image: docker://rust:1.60.0
-    steps:
-      - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
-      - run: rustup component add rustfmt
-      - run: cargo fmt --all -- --check
-
-  audit:
-    timeout-minutes: 10
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        checks:
-          - advisories
-          - bans licenses sources
-    # Prevent sudden announcement of a new advisory from failing Ci.
-    continue-on-error: ${{ matrix.checks == 'advisories' }}
-    steps:
-    - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
-    - uses: EmbarkStudios/cargo-deny-action@30ecad1d5873c1cc2cad10a33637371ca094768b
-      with:
-        command: check ${{ matrix.checks }}
-
-  clippy:
-    timeout-minutes: 10
-    runs-on: ubuntu-latest
-    container:
-      image: docker://rust:1.60.0
-    steps:
-      - run: rustup component add clippy
-      - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
-      - uses: olix0r/cargo-action-fmt@ee1ef42932e44794821dab57ef1bf7a73df8b21f
-      - run: cargo fetch --locked
-      - name: Run cargo clippy
-        run: cargo clippy --frozen --all --no-deps --message-format=json | cargo-action-fmt
-
-  check:
-    timeout-minutes: 20
-    runs-on: ubuntu-latest
-    container:
-      image: docker://rust:1.60.0
-    steps:
-      - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
-      - uses: olix0r/cargo-action-fmt@ee1ef42932e44794821dab57ef1bf7a73df8b21f
-      - run: cargo fetch --locked
-      # Check each crate independently to ensure its Cargo.toml is sufficient.
-      - run: |
-          for toml in $(find . -mindepth 2 -name Cargo.toml | sort -r)
-          do
-            d=$(dirname "$toml")
-            echo "# $d"
-            (cd $d ; cargo check --all-targets --frozen --message-format=json | cargo-action-fmt)
-          done
-
-  test:
-    name: test
-    runs-on: ubuntu-latest
-    timeout-minutes: 15
-    container:
-      image: docker://rust:1.60.0
-    steps:
-      - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
-      - run: cargo fetch --locked
-      - name: Build tests
-        run: cargo test --workspace --exclude=linkerd-policy-test --frozen --no-run
-      - name: Run cargo test
-        run: cargo test --workspace --exclude=linkerd-policy-test --frozen
-
-  rust-toolchain:
-    name: rust toolchain
-    runs-on: ubuntu-latest
-    timeout-minutes: 2
-    steps:
-      - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
-      - run: |
-          ex=0
-
-          # Check this workflow against the version in rust-toolchain.
-          versions=$(sed -nE 's|.*docker://(.*/)?rust:([^ #]+).*|\2|p' .github/workflows/policy_controller.yml)
-          for mismatch in $(echo "$versions" | grep -vF "$(cat rust-toolchain)" || true) ; do
-            echo "::error file=.github/workflows/policy_controller.yml::Workflow uses incorrect rust version(s): $mismatch"
-            ex=$((ex + 1))
-          done
-
-          # Check the policy-controller dockerfiles workflow against the version
-          # in rust-toolchain.
-          for f in policy-controller/*.dockerfile ; do
-            versions=$(sed -nE 's|ARG RUST_IMAGE=(.*/)?rust:([^ #]+).*|\2|p' "$f")
-            for mismatch in $(echo "$versions" | grep -vF "$(cat rust-toolchain)" || true) ; do
-              echo "::error file=\"$f\"::$f uses incorrect rust version(s): $mismatch"
-              ex=$((ex + 1))
-            done
-          done
-
-          exit $ex
-
   docker-build:
     runs-on: ubuntu-20.04
     strategy:
diff --git a/.github/workflows/rust_checks.yml b/.github/workflows/rust_checks.yml
new file mode 100644
index 000000000..0b4ab30e2
--- /dev/null
+++ b/.github/workflows/rust_checks.yml
@@ -0,0 +1,121 @@
+name: Rust Static checks
+
+on:
+  pull_request:
+    paths:
+      - .github/workflows/rust_checks.yml
+      - Cargo.lock
+      - '**/Cargo.toml'
+      - deny.toml
+      - '**/*.rs'
+      - policy-*/*.dockerfile
+      - rust-toolchain
+
+permissions:
+  contents: read
+
+env:
+  CARGO_INCREMENTAL: 0
+  CARGO_NET_RETRY: 10
+  PROTOC_NO_VENDOR: 1
+  RUST_BACKTRACE: short
+  RUSTUP_MAX_RETRIES: 10
+jobs:
+  fmt:
+    timeout-minutes: 5
+    runs-on: ubuntu-latest
+    container:
+      image: docker://rust:1.60.0
+    steps:
+      - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
+      - run: rustup component add rustfmt
+      - run: cargo fmt --all -- --check
+
+  audit:
+    timeout-minutes: 10
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        checks:
+          - advisories
+          - bans licenses sources
+    # Prevent sudden announcement of a new advisory from failing Ci.
+    continue-on-error: ${{ matrix.checks == 'advisories' }}
+    steps:
+    - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
+    - uses: EmbarkStudios/cargo-deny-action@30ecad1d5873c1cc2cad10a33637371ca094768b
+      with:
+        command: check ${{ matrix.checks }}
+
+  clippy:
+    timeout-minutes: 10
+    runs-on: ubuntu-latest
+    container:
+      image: docker://rust:1.60.0
+    steps:
+      - run: rustup component add clippy
+      - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
+      - uses: olix0r/cargo-action-fmt@ee1ef42932e44794821dab57ef1bf7a73df8b21f
+      - run: cargo fetch --locked
+      - name: Run cargo clippy
+        run: cargo clippy --frozen --workspace --all-targets --message-format=json | cargo-action-fmt
+
+  check:
+    timeout-minutes: 20
+    runs-on: ubuntu-latest
+    container:
+      image: docker://rust:1.60.0
+    steps:
+      - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
+      - uses: olix0r/cargo-action-fmt@ee1ef42932e44794821dab57ef1bf7a73df8b21f
+      - run: cargo fetch --locked
+      # Check each crate independently to ensure its Cargo.toml is sufficient.
+      - run: |
+          for toml in $(find . -mindepth 2 -name Cargo.toml | sort -r)
+          do
+            d=$(dirname "$toml")
+            echo "# $d"
+            (cd $d ; cargo check --all-targets --frozen --message-format=json | cargo-action-fmt)
+          done
+
+  test:
+    name: test
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    container:
+      image: docker://rust:1.60.0
+    steps:
+      - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
+      - run: cargo fetch --locked
+      - name: Build tests
+        run: cargo test --workspace --exclude=linkerd-policy-test --frozen --no-run
+      - name: Run cargo test
+        run: cargo test --workspace --exclude=linkerd-policy-test --frozen
+
+  rust-toolchain:
+    name: rust toolchain
+    runs-on: ubuntu-latest
+    timeout-minutes: 2
+    steps:
+      - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
+      - run: |
+          ex=0
+
+          # Check this workflow against the version in rust-toolchain.
+          versions=$(sed -nE 's|.*docker://(.*/)?rust:([^ #]+).*|\2|p' .github/workflows/*)
+          for mismatch in $(echo "$versions" | grep -vF "$(cat rust-toolchain)" || true) ; do
+            echo "::error file=.github/workflows/rust_checks.yml::Workflow uses incorrect rust version(s): $mismatch"
+            ex=$((ex + 1))
+          done
+
+          # Check the policy-controller dockerfiles workflow against the version
+          # in rust-toolchain.
+          for f in policy-controller/*.dockerfile ; do
+            versions=$(sed -nE 's|ARG RUST_IMAGE=(.*/)?rust:([^ #]+).*|\2|p' "$f")
+            for mismatch in $(echo "$versions" | grep -vF "$(cat rust-toolchain)" || true) ; do
+              echo "::error file=\"$f\"::$f uses incorrect rust version(s): $mismatch"
+              ex=$((ex + 1))
+            done
+          done
+
+          exit $ex
diff --git a/Cargo.lock b/Cargo.lock
index 212d314a8..d357732db 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1218,9 +1218,9 @@ dependencies = [
 
 [[package]]
 name = "os_str_bytes"
-version = "6.0.1"
+version = "6.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435"
+checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
 
 [[package]]
 name = "parking_lot"
diff --git a/bin/_docker.sh b/bin/_docker.sh
index b475ffc54..defc9605a 100644
--- a/bin/_docker.sh
+++ b/bin/_docker.sh
@@ -45,7 +45,7 @@ docker_repo() {
     repo=$1
 
     name=$repo
-    if [ -n "${DOCKER_REGISTRY:-}" ]; then
+    if [ "${DOCKER_REGISTRY:-}" ]; then
         name="$DOCKER_REGISTRY/$name"
     fi
 
@@ -65,19 +65,19 @@ docker_build() {
     rootdir=${ROOTDIR:-$( cd "$bindir"/.. && pwd )}
     cache_params=""
 
-    if [ -n "$DOCKER_BUILDKIT_CACHE" ]; then
+    if [ "$DOCKER_BUILDKIT_CACHE" ]; then
       cache_params="--cache-from type=local,src=${DOCKER_BUILDKIT_CACHE} --cache-to type=local,dest=${DOCKER_BUILDKIT_CACHE},mode=max"
     fi
 
     output_params="--load"
     if [ "$DOCKER_TARGET" = 'multi-arch' ]; then
       output_params="--platform $SUPPORTED_ARCHS"
-      if [ -n "$DOCKER_PUSH" ]; then
+      if [ "$DOCKER_PUSH" ]; then
         output_params+=" --push"
       else
-        echo "Error: env DOCKER_PUSH=1 is missing"
-        echo "When building the multi-arch images it is required to push the images to the registry"
-        echo "See https://github.com/docker/buildx/issues/59 for more details"
+        echo 'Error: env DOCKER_PUSH=1 is missing
+When building the multi-arch images it is required to push the images to the registry
+See https://github.com/docker/buildx/issues/59 for more details'
         exit 1
       fi
     fi
diff --git a/bin/_log.sh b/bin/_log.sh
index 937b49561..f89487638 100644
--- a/bin/_log.sh
+++ b/bin/_log.sh
@@ -5,12 +5,12 @@ set -eu
 # shell trace logging is disabled by default; enable with TRACE=1
 
 export TRACE="${TRACE:-}"
-if [ -n "$TRACE" ]; then
+if [ "$TRACE" ]; then
     set -x
 fi
 
 log_debug() {
-    if [ -z "$TRACE" ] && [ -n "${BUILD_DEBUG:-}" ]; then
+    if [ -z "$TRACE" ] && [ "${BUILD_DEBUG:-}" ]; then
         echo "$@" >&2
     fi
 }
diff --git a/bin/_os.sh b/bin/_os.sh
index 32e7207d9..49a1db212 100644
--- a/bin/_os.sh
+++ b/bin/_os.sh
@@ -20,7 +20,7 @@ architecture() {
       arch=arm
       ;;
     amd64|arm64)
-      arch=$arch
+      # keep arch as is
       ;;
     *)
       echo "unsupported architecture: $arch" >&2
@@ -50,7 +50,7 @@ os() {
       ;;
   esac
 
-  if [ -n "$arch" ]; then
+  if [ "$arch" ]; then
     echo "$os-$arch"
   else
     echo "$os"
diff --git a/bin/_tag.sh b/bin/_tag.sh
index c843ded79..8f700e5bc 100644
--- a/bin/_tag.sh
+++ b/bin/_tag.sh
@@ -7,7 +7,7 @@ git_sha_head() {
 }
 
 clean_head() {
-    [ -n "${CI_FORCE_CLEAN:-}" ] || git diff-index --quiet HEAD --
+    [ "${CI_FORCE_CLEAN:-}" ] || git diff-index --quiet HEAD --
 }
 
 named_tag() {
diff --git a/bin/create-release-tag b/bin/create-release-tag
index 4f6467443..38e41db2a 100755
--- a/bin/create-release-tag
+++ b/bin/create-release-tag
@@ -7,10 +7,10 @@ bindir=$( cd "${0%/*}" && pwd )
 
 # Assert at least one and at most two arguments were passed.
 if [[ $# -lt 1 || $# -gt 2 ]]; then
-    echo "Error: ${0##*/} accepts 1 or 2 arguments" >&2
-    echo "Usage:" >&2
-    echo "    ${0##*/} edge" >&2
-    echo "    ${0##*/} stable x.x.x" >&2
+    echo "Error: ${0##*/} accepts 1 or 2 arguments
+Usage:
+    ${0##*/} edge
+    ${0##*/} stable x.x.x" >&2
     exit 1
 fi
 
@@ -19,29 +19,29 @@ release_channel="$1"
 # Verify the release channel.
 release_channel_regex="^(edge|stable)$"
 if [[ ! $release_channel =~ $release_channel_regex ]]; then
-    echo "Error: valid release channels: edge, stable"
-    echo "Usage:"
-    echo "    bin/create-release-tag edge"
-    echo "    bin/create-release-tag stable 2.4.8"
+    echo 'Error: valid release channels: edge, stable
+Usage:
+    bin/create-release-tag edge
+    bin/create-release-tag stable 2.4.8'
     exit 1
 fi
 
-if [ "$release_channel" == "stable" ]; then
+if [ "$release_channel" = stable ]; then
     if [ $# -ne 2 ]; then
-        echo "Error: accepts 2 arguments"
-        echo "Usage:"
-        echo "    bin/create-release-tag stable 2.4.8"
+        echo 'Error: accepts 2 arguments
+Usage:
+    bin/create-release-tag stable 2.4.8'
         exit 1
     fi
 
-    stable_tag="$2"
+    stable_tag=$2
 
     # Verify the stable tag format.
     stable_tag_regex="^([0-9]+)\.([0-9]+)\.([0-9]+)$"
     if [[ ! $stable_tag =~ $stable_tag_regex ]]; then
-        echo "Error: version reference incorrect"
-        echo "Usage:"
-        echo "    bin/create-release-tag stable 2.4.8"
+        echo 'Error: version reference incorrect
+Usage:
+    bin/create-release-tag stable 2.4.8'
         exit 1
     fi
 
@@ -49,18 +49,18 @@ if [ "$release_channel" == "stable" ]; then
     release_tag="stable-$stable_tag"
 fi
 
-if [ "$release_channel" == "edge" ]; then
+if [ "$release_channel" = edge ]; then
 if [ $# -ne 1 ]; then
-        echo "Error: accepts 1 argument"
-        echo "Usage:"
-        echo "    bin/create-release-tag edge"
+        echo 'Error: accepts 1 argument
+Usage:
+    bin/create-release-tag edge'
         exit 1
     fi
 
     # Some awks on Linux do no support quantifiers in regex, so this regex is
     # explicit.
     edge_tag_regex="(edge)-([0-9][0-9])\.([0-9]|[0-9][0-9])\.([0-9]+)"
-    url="https://run.linkerd.io/install-edge"
+    url=https://run.linkerd.io/install-edge
 
     # Get the current edge version.
     current_edge=$("$bindir"/scurl $url | awk -v tag_format="$edge_tag_regex" '$0 ~ tag_format')
@@ -76,8 +76,8 @@ if [ $# -ne 1 ]; then
     new_mm=$(date +"%-m")
 
     # If this is a new month, `new_xx` should be 1; otherwise increment it.
-    if [ ! "$new_mm" == "$current_mm" ]; then
-        new_xx="1"
+    if [ "$new_mm" != "$current_mm" ]; then
+        new_xx=1
     else
         new_xx=$((current_xx+1))
     fi
@@ -94,9 +94,9 @@ tmp=$(. "$bindir"/_release.sh; extract_release_notes)
 git tag -s -F "$tmp" "$release_tag"
 
 # Success message
-echo "$release_tag tag created and signed."
-echo ""
-echo "tag: $release_tag"
-echo ""
-echo "To push tag, run:"
-echo "    git push origin $release_tag"
+echo "$release_tag tag created and signed.
+
+tag: $release_tag
+
+To push tag, run:
+    git push origin $release_tag"
diff --git a/bin/git-commit-proxy-version b/bin/git-commit-proxy-version
index e605cbdc1..35be894c8 100755
--- a/bin/git-commit-proxy-version
+++ b/bin/git-commit-proxy-version
@@ -43,15 +43,15 @@ fi
 
 # Commit message
 (
-    echo "proxy: ${new_proxy_version}"
-    echo ""
+    echo "proxy: ${new_proxy_version}
+"
     # Include the log message, stripping the version header and GPG trailer.
     git tag -l --format='%(contents)' "$new_proxy_rev" | \
         sed -e 1,2d -e '/^---*BEGIN /,$d'
-    echo ""
+    echo ''
     # Include the history, with references to pull requests.
-    echo "---"
-    echo ""
+    echo "---
+"
     refs="${old_proxy_rev}..${new_proxy_rev}"
     git log --pretty=oneline --abbrev-commit --reverse "$refs" | \
         sed -Ee 's,^[a-f0-9]{7} ,* ,' \
diff --git a/bin/helm-build b/bin/helm-build
index a5af06dc9..0937f2245 100755
--- a/bin/helm-build
+++ b/bin/helm-build
@@ -12,7 +12,7 @@ setValues() {
 
 showErr() {
   printf "Error on exit:\n  Exit code: %d\n  Failed command: \"%s\"\n" $? "$BASH_COMMAND"
-  setValues "$fullVersion" "linkerdVersionValue"
+  setValues "$fullVersion" 'linkerdVersionValue'
 }
 
 # trap the last failed command
@@ -64,7 +64,7 @@ if [ "$1" = package ]; then
     version=${BASH_REMATCH[2]}
 
     # set version in Values files
-    setValues "linkerdVersionValue" "$fullVersion"
+    setValues 'linkerdVersionValue' "$fullVersion"
 
     "$bindir"/helm -d "$rootdir"/target/helm package "$rootdir"/charts/linkerd-crds
     "$bindir"/helm --app-version "$tag" -d "$rootdir"/target/helm package "$rootdir"/charts/linkerd-control-plane
@@ -77,5 +77,5 @@ if [ "$1" = package ]; then
     "$bindir"/helm repo index --url "https://helm.linkerd.io/$repo/" --merge "$rootdir"/target/helm/index-pre-"$version".yaml "$rootdir"/target/helm
 
     # restore version in Values files
-    setValues "$fullVersion" "linkerdVersionValue"
+    setValues "$fullVersion" 'linkerdVersionValue'
 fi
diff --git a/bin/image-load b/bin/image-load
index 0b174886f..cf222f6f6 100755
--- a/bin/image-load
+++ b/bin/image-load
@@ -2,11 +2,11 @@
 
 set -eo pipefail
 
-kind=""
-k3d=""
-cluster=""
-archive=""
-preload=""
+kind=''
+k3d=''
+cluster=''
+archive=''
+preload=''
 images=()
 
 bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
@@ -15,27 +15,27 @@ bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
 
 usage() {
   printf "Load into KinD/k3d the referred Linkerd images. If no images are specified all of them are loaded (%s)\n" "${DOCKER_IMAGES[*]}"
-  echo ""
-  echo "Usage:"
-  echo "    bin/image-load [--kind] [--k3d] [--cluster name] [--preload] [--archive] [image] [image]..."
-  echo ""
-  echo "Examples:"
-  echo ""
-  echo "    # Load all the images from the local docker instance into KinD"
-  echo "    bin/image-load"
-  echo ""
-  echo "    # Load only the proxy and controller images into k3d"
-  echo "    bin/image-load --k3d proxy controller"
-  echo ""
-  echo "    # Load all the images from tar files located under the 'image-archives' directory into k3d"
-  echo "    bin/image-load --k3d --archive"
-  echo ""
-  echo "Available Commands:"
-  echo "    --cluster: target cluster name (defaults to 'k3s-default' under k3d, and 'kind' under KinD)."
-  echo "    --kind: use a KinD cluster (default)."
-  echo "    --k3d: use a k3d cluster."
-  echo "    --preload: pull the docker images from a public registry prior to loading them into the cluster, which appears to be faster than having k3d pulling them itself."
-  echo "    --archive: load the images from local .tar files in the current directory."
+  echo "
+Usage:
+    bin/image-load [--kind] [--k3d] [--cluster name] [--preload] [--archive] [image] [image]...
+
+Examples:
+
+    # Load all the images from the local docker instance into KinD
+    bin/image-load
+
+    # Load only the proxy and controller images into k3d
+    bin/image-load --k3d proxy controller
+
+    # Load all the images from tar files located under the 'image-archives' directory into k3d
+    bin/image-load --k3d --archive
+
+Available Commands:
+    --cluster: target cluster name (defaults to 'k3s-default' under k3d, and 'kind' under KinD).
+    --kind: use a KinD cluster (default).
+    --k3d: use a k3d cluster.
+    --preload: pull the docker images from a public registry prior to loading them into the cluster, which appears to be faster than having k3d pulling them itself.
+    --archive: load the images from local .tar files in the current directory."
 }
 
 while :
@@ -81,15 +81,15 @@ fi
 
 bindir=$( cd "${0%/*}" && pwd )
 
-if [ -n "$k3d" ]; then
-  if [ -n "$kind" ]; then
-    echo $k3d
+if [ "$k3d" ]; then
+  if [ "$kind" ]; then
+    echo "$k3d"
     echo "Error: --kind and --k3d can't be used simultaneously" >&2
     usage
     exit 1
   fi
   if [ -z "$cluster" ]; then
-    cluster="k3s-default"
+    cluster=k3s-default
   fi
   bin="$bindir"/k3d
   image_sub_cmd=(image import -c "$cluster")
@@ -97,7 +97,7 @@ else
   kind=1
   bin="$bindir"/kind
   if [ -z "$cluster" ]; then
-    cluster="kind"
+    cluster=kind
   fi
   if [ $archive ]; then
     image_sub_cmd=(load image-archive --name "$cluster")
@@ -130,7 +130,7 @@ for i in "${!images[@]}"; do
     fi
   fi
 
-  if [ -n "$kind" ]; then
+  if [ "$kind" ]; then
     printf 'Importing %s...\n' "${images[$i]}"
     "$bin" "${image_sub_cmd[@]}" "${param[@]}" || touch load_fail &
   else
@@ -142,12 +142,12 @@ done
 
 wait < <(jobs -p)
 if [ -f load_fail ]; then
-  echo "Loading docker images into the cluster failed."
+  echo 'Loading docker images into the cluster failed.'
   rm load_fail
   exit 1
 fi
 
-if [ -n "$k3d" ]; then
+if [ "$k3d" ]; then
   printf 'Importing %s...\n' "${images[@]}"
   "$bin" "${image_sub_cmd[@]}" "${images[@]}" -m tools-node
 fi
diff --git a/bin/install-pr b/bin/install-pr
index bbc749156..ebaf57421 100755
--- a/bin/install-pr
+++ b/bin/install-pr
@@ -11,7 +11,7 @@
 
 set -eo pipefail
 
-cluster=""
+cluster=''
 is_kind=false
 is_k3d=false
 
@@ -20,22 +20,22 @@ while :
 do
   case $1 in
     -h|--help)
-      echo "Install Linkerd with the changes made in a GitHub Pull Request."
-      echo ""
-      echo "Usage:"
-      echo "    --cluster: The name of the cluster to use"
-      echo ""
-      echo "    # Install Linkerd into the current cluster"
-      echo "    bin/install-pr 1234"
-      echo ""
-      echo "    # Install Linkerd into the current k3d cluster"
-      echo "    bin/install-pr --k3d 1234"
-      echo ""
-      echo "    # Install Linkerd into the current KinD cluster"
-      echo "    bin/install-pr --kind 1234"
-      echo ""
-      echo "    # Install Linkerd into the 'pr-1234' k3d cluster"
-      echo "    bin/install-pr --k3d --cluster pr-1234 1234"
+      echo "Install Linkerd with the changes made in a GitHub Pull Request.
+
+Usage:
+    --cluster: The name of the cluster to use
+
+    # Install Linkerd into the current cluster
+    bin/install-pr 1234
+
+    # Install Linkerd into the current k3d cluster
+    bin/install-pr --k3d 1234
+
+    # Install Linkerd into the current KinD cluster
+    bin/install-pr --kind 1234
+
+    # Install Linkerd into the 'pr-1234' k3d cluster
+    bin/install-pr --k3d --cluster pr-1234 1234"
       exit 0
       ;;
     --cluster)
@@ -62,9 +62,9 @@ pr=$1
 
 if [ -z "$pr" ]
 then
-  echo "Error: ${0##*/} accepts 1 argument" >&2
-  echo "Usage:" >&2
-  echo "    ${0##*/} ####" >&2
+  echo "Error: ${0##*/} accepts 1 argument
+Usage:
+    ${0##*/} ####" >&2
   exit 1
 fi
 
@@ -75,8 +75,8 @@ then
   exit 1
 fi
 
-linkerd2_pulls_url="https://api.github.com/repos/linkerd/linkerd2/pulls"
-linkerd2_integration_url="https://api.github.com/repos/linkerd/linkerd2/actions/workflows/integration_tests.yml"
+linkerd2_pulls_url='https://api.github.com/repos/linkerd/linkerd2/pulls'
+linkerd2_integration_url='https://api.github.com/repos/linkerd/linkerd2/actions/workflows/integration_tests.yml'
 
 bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
 
@@ -93,12 +93,12 @@ dir=$(mktemp -d -t "linkerd-pr-$pr.XXXXXXXXXX")
 
 cd "$dir" || exit
 
-echo "### Downloading images ###"
+echo '### Downloading images ###'
 
 "$bindir"/scurl -o archive.zip -H "$auth" "$archive"
 unzip -o archive.zip -d image-archives/
 
-echo "### Loading images into Docker ###"
+echo '### Loading images into Docker ###'
 
 image=$(docker load -i image-archives/cli-bin.tar | sed 's/Loaded image: //')
 tag=$(echo "$image" | cut -f 2 -d ":")
@@ -113,11 +113,11 @@ then
   # When importing into k3d or kind, the images must be renamed to use the
   # proper registry so that users don't have to change the default install
   # output.
-  docker_rename_registry "$tag" "ghcr.io/linkerd" "cr.l5d.io/linkerd"
-  distro="k3d"
+  docker_rename_registry "$tag" 'ghcr.io/linkerd' 'cr.l5d.io/linkerd'
+  distro=k3d
   if [ "$is_kind" = true ]
   then
-    distro="kind"
+    distro=kind
   fi
   export TAG="$tag"
   "$bindir"/image-load --"$distro" --cluster "$cluster"
@@ -136,13 +136,13 @@ rm -rf "$dir"
 
 case $(uname) in
   Darwin)
-    platform="darwin"
+    platform=darwin
     ;;
   Linux)
-    platform="linux"
+    platform=linux
     ;;
   *)
-    platform="windows"
+    platform=windows
     ;;
 esac
 
@@ -151,6 +151,6 @@ esac
 export DOCKER_REGISTRY=ghcr.io/linkerd
 linkerd=$("$bindir"/docker-pull-binaries "$tag" | awk -v platform=$platform '$0 ~ platform')
 
-echo ""
-echo "Linkerd CLI available:"
-echo "$linkerd"
+echo "
+Linkerd CLI available:
+$linkerd"
diff --git a/bin/web b/bin/web
index f7b9077ba..0444d16ac 100755
--- a/bin/web
+++ b/bin/web
@@ -34,7 +34,7 @@ check-for-linkerd-and-viz() {
 }
 
 dev() {
-  while getopts "p:" opt; do
+  while getopts 'p:' opt; do
     case "$opt" in
       p) DEV_PORT=$OPTARG;;
       *) ;;
@@ -58,7 +58,7 @@ get-pod() {
 
 
   selector="linkerd.io/control-plane-component=$2"
-  if [ "$1" == "linkerd-viz" ]; then
+  if [ "$1" = 'linkerd-viz' ]; then
     selector="component=$2"
   fi
 
@@ -74,8 +74,8 @@ port-forward() {
     exit 1
   fi
 
-  port_from=""
-  port_to=""
+  port_from=''
+  port_to=''
   if [ $# -eq 4 ]; then
     port_from=$3
     port_to=$4
diff --git a/cni-plugin/deployment/scripts/install-cni.sh b/cni-plugin/deployment/scripts/install-cni.sh
index a3a06a4a1..abc568cba 100755
--- a/cni-plugin/deployment/scripts/install-cni.sh
+++ b/cni-plugin/deployment/scripts/install-cni.sh
@@ -67,8 +67,8 @@ KUBECONFIG_FILE_NAME=${KUBECONFIG_FILE_NAME:-ZZZ-linkerd-cni-kubeconfig}
 # file will be removed.
 cleanup() {
   # First, kill 'inotifywait' so we don't process any DELETE/CREATE events
-  if [ -n "$(pgrep inotifywait)" ]; then
-    echo "Sending SIGKILL to inotifywait"
+  if [ "$(pgrep inotifywait)" ]; then
+    echo 'Sending SIGKILL to inotifywait'
     kill -s KILL "$(pgrep inotifywait)"
   fi
 
@@ -77,7 +77,7 @@ cleanup() {
   # Find all conflist files and print them out using a NULL separator instead of
   # writing each file in a new line. We will subsequently read each string and
   # attempt to rm linkerd config from it using jq helper.
-  local cni_data=""
+  local cni_data=''
   find "${HOST_CNI_NET}" -maxdepth 1 -type f \( -iname '*conflist' \) -print0 |
     while read -r -d $'\0' file; do
       echo "Removing linkerd-cni config from $file"
@@ -139,7 +139,7 @@ create_cni_conf() {
   if [ -e "${CNI_NETWORK_CONFIG_FILE}" ]; then
     echo "Using CNI config template from ${CNI_NETWORK_CONFIG_FILE}."
     cp "${CNI_NETWORK_CONFIG_FILE}" "${TMP_CONF}"
-  elif [ "${CNI_NETWORK_CONFIG}" != "" ]; then
+  elif [ "${CNI_NETWORK_CONFIG}" ]; then
     echo 'Using CNI config template from CNI_NETWORK_CONFIG environment variable.'
     cat >"${TMP_CONF}" <<EOF
 ${CNI_NETWORK_CONFIG}
@@ -219,11 +219,11 @@ EOF
 }
 
 install_cni_conf() {
-  local cni_conf_path="$1"
+  local cni_conf_path=$1
  
   create_cni_conf
-  local tmp_data=""
-  local conf_data=""
+  local tmp_data=''
+  local conf_data=''
   if [ -e "${cni_conf_path}" ]; then
    # Add the linkerd-cni plugin to the existing list
    tmp_data=$(cat "${TMP_CONF}")
@@ -233,7 +233,7 @@ install_cni_conf() {
 
   # If the old config filename ends with .conf, rename it to .conflist, because it has changed to be a list
   filename=${cni_conf_path##*/}
-  extension="${filename##*.}"
+  extension=${filename##*.}
   if [ "${filename}" != '01-linkerd-cni.conf' ] && [ "${extension}" = 'conf' ]; then
    echo "Renaming ${cni_conf_path} extension to .conflist"
    cni_conf_path="${cni_conf_path}list"
@@ -258,15 +258,15 @@ install_cni_conf() {
 # Based on the changed file and event type, sync() might re-install the CNI
 # plugin's configuration file.
 sync() {
-  local filename="$1"
-  local ev="$2"
+  local filename=$1
+  local ev=$2
   local filepath="${HOST_CNI_NET}/$filename"
 
-  local prev_sha="$3"
+  local prev_sha=$3
 
   local config_file_count
   local new_sha
-  if [ "$ev" = "DELETE" ]; then
+  if [ "$ev" = 'DELETE' ]; then
     # When the event type is 'DELETE', we check to see if there are any `*conf` or `*conflist`
     # files on the host's filesystem. If none are present, we install in
     # 'interface' mode, using our own CNI config file.
@@ -275,11 +275,11 @@ sync() {
       echo "No active CNI configuration file found after $ev event; re-installing in \"interface\" mode"
       install_cni_conf "${DEFAULT_CNI_CONF_PATH}"
     fi
-  elif [ "$ev" = "CREATE" ]; then
+  elif [ "$ev" = 'CREATE' ]; then
     # When the event type is 'CREATE', we check the previously observed SHA (updated
     # with each file watch) and compare it against the new file's SHA. If they
     # differ, it means something has changed.
-    new_sha=$(sha256sum "${filepath}" | awk '{print $1}')
+    new_sha=$(sha256sum "${filepath}" | while read -r s _; do echo "$s"; done)
     if [ "$new_sha" != "$prev_sha" ]; then
       # Create but don't rm old one since we don't know if this will be configured
       # to run as _the_ cni plugin.
@@ -309,7 +309,7 @@ monitor() {
         # When file exists (i.e we didn't deal with a DELETE ev)
         # then calculate its sha to be used the next turn.
         if [ -e "$directory/$filename" ]; then
-          cni_conf_sha="$(sha256sum "$directory/$filename" | awk '{print $1}')"
+          cni_conf_sha="$(sha256sum "$directory/$filename" | while read -r s _; do echo "$s"; done)"
         fi
       fi
     done
@@ -324,7 +324,7 @@ install_cni_bin
 # Install CNI configuration. If we have an existing CNI configuration file (*.conflist or *.conf) that is not linkerd's,
 # then append our configuration to that file. Otherwise, if no CNI config files
 # are present, install our stand-alone config file.
-config_file_count=$(find "${HOST_CNI_NET}" -maxdepth 1 -type f \( -iname '*conflist' -o -iname '*conf' \) | grep -v "linkerd" | sort | wc -l)
+config_file_count=$(find "${HOST_CNI_NET}" -maxdepth 1 -type f \( -iname '*conflist' -o -iname '*conf' \) | grep -v linkerd | sort | wc -l)
 if [ "$config_file_count" -eq 0 ]; then
   echo "No active CNI configuration files found; installing in \"interface\" mode in ${DEFAULT_CNI_CONF_PATH}"
   install_cni_conf "${DEFAULT_CNI_CONF_PATH}"
@@ -339,7 +339,7 @@ fi
 # Compute SHA for first config file found; this will be updated after every iteration.
 # First config file is likely to be chosen as the de facto CNI config by the
 # host.
-cni_conf_sha="$(sha256sum "$(find "${HOST_CNI_NET}" -maxdepth 1 -type f \( -iname '*conflist' -o -iname '*conf' \) | sort | head -n 1)" | awk '{print $1}')"
+cni_conf_sha="$(sha256sum "$(find "${HOST_CNI_NET}" -maxdepth 1 -type f \( -iname '*conflist' -o -iname '*conf' \) | sort | head -n 1)" | while read -r s _; do echo "$s"; done)"
 
 # Watch in bg so we can receive interrupt signals through 'trap'. From 'man
 # bash': 
diff --git a/go.mod b/go.mod
index b5a81769b..cdb8ac687 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
 	github.com/gorilla/websocket v1.5.0
 	github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
-	github.com/imdario/mergo v0.3.12
+	github.com/imdario/mergo v0.3.13
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/linkerd/linkerd2-proxy-api v0.5.0
 	github.com/linkerd/linkerd2-proxy-init v1.5.3
@@ -157,7 +157,7 @@ require (
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.0 // indirect
 	k8s.io/cli-runtime v0.24.0 // indirect
 	k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 // indirect
 	k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect
diff --git a/go.sum b/go.sum
index 48067ec72..ba410d974 100644
--- a/go.sum
+++ b/go.sum
@@ -596,8 +596,9 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ=
@@ -1630,8 +1631,9 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
diff --git a/pkg/profiles/proto.go b/pkg/profiles/proto.go
index 7a5489ddc..26c3d48ea 100644
--- a/pkg/profiles/proto.go
+++ b/pkg/profiles/proto.go
@@ -5,6 +5,7 @@ import (
 	"io"
 	"net/http"
 	"regexp"
+	"strings"
 
 	"github.com/emicklei/proto"
 	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
@@ -52,12 +53,14 @@ func protoToServiceProfile(parser *proto.Parser, namespace, name, clusterDomain
 				default:
 					path = fmt.Sprintf("/%s.%s/%s", pkg, service.Name, typed.Name)
 				}
+
 				route := &sp.RouteSpec{
 					Name: typed.Name,
 					Condition: &sp.RequestMatch{
 						Method:    http.MethodPost,
 						PathRegex: regexp.QuoteMeta(path),
 					},
+					IsRetryable: isMethodRetryable(typed),
 				}
 				routes = append(routes, route)
 			}
@@ -77,3 +80,24 @@ func protoToServiceProfile(parser *proto.Parser, namespace, name, clusterDomain
 		},
 	}, nil
 }
+
+func isMethodRetryable(rpc *proto.RPC) bool {
+	for _, e := range rpc.Elements {
+		option, ok := e.(*proto.Option)
+		if !ok {
+			// Not an option
+			continue
+		}
+
+		// method options can be wrapped in parentheses so we trim them away
+		if strings.Trim(option.Name, "()") != "idempotency_level" {
+			// Not an idempotency option
+			continue
+		}
+
+		return option.Constant.Source == "IDEMPOTENT" ||
+			option.Constant.Source == "NO_SIDE_EFFECTS"
+	}
+
+	return false
+}


Test patch:
diff --git a/bin/_test-helpers.sh b/bin/_test-helpers.sh
index a83bc21cd..511f41b8e 100644
--- a/bin/_test-helpers.sh
+++ b/bin/_test-helpers.sh
@@ -63,14 +63,14 @@ Available Commands:
 }
 
 handle_tests_input() {
-  export images="docker"
+  export images=docker
   export test_name=''
   export skip_cluster_create=''
   export skip_cluster_delete=''
   export cleanup_docker=''
   export linkerd_path=""
 
-  while  [ "$#" -ne 0 ]; do
+  while  [ $# -ne 0 ]; do
     case $1 in
       -h|--help)
         tests_usage "$0"
@@ -83,7 +83,7 @@ handle_tests_input() {
           tests_usage "$0" >&2
           exit 64
         fi
-        if [[ $images != "docker" && $images != "archive" && $images != "preload" ]]; then
+        if [[ $images != 'docker' && $images != 'archive' && $images != 'preload' ]]; then
           echo 'Error: the argument for --images was invalid' >&2
           tests_usage "$0" >&2
           exit 64
@@ -119,37 +119,37 @@ handle_tests_input() {
           tests_usage "$0" >&2
           exit 64
         fi
-        if [ -n "$linkerd_path" ]; then
-          echo "Multiple linkerd paths specified:" >&2
+        if [ "$linkerd_path" ]; then
+          echo 'Multiple linkerd paths specified:' >&2
           echo "  $linkerd_path" >&2
           echo "  $1" >&2
           tests_usage "$0" >&2
           exit 64
         fi
-        linkerd_path="$1"
+        linkerd_path=$1
         shift
         ;;
     esac
   done
 
   if [ -z "$linkerd_path" ]; then
-    echo "Error: path to linkerd binary is required" >&2
+    echo 'Error: path to linkerd binary is required' >&2
     tests_usage "$0" >&2
     exit 64
   fi
 
-  if [ -z "$test_name" ] && [ -n "$skip_cluster_delete" ]; then
-    echo "Error: must provide --name when using --skip-cluster-delete" >&2
+  if [ -z "$test_name" ] && [ "$skip_cluster_delete" ]; then
+    echo 'Error: must provide --name when using --skip-cluster-delete' >&2
     tests_usage "$0" >&2
     exit 64
   fi
 }
 
 handle_cleanup_input() {
-  export k8s_context=""
-  export linkerd_path=""
+  export k8s_context=''
+  export linkerd_path=''
 
-  while  [ "$#" -ne 0 ]; do
+  while  [ $# -ne 0 ]; do
     case $1 in
       -h|--help)
         cleanup_usage "$0"
@@ -166,21 +166,21 @@ handle_cleanup_input() {
           cleanup_usage "$0" >&2
           exit 64
         fi
-        if [ -n "$linkerd_path" ]; then
-          echo "Multiple linkerd paths specified:" >&2
+        if [ "$linkerd_path" ]; then
+          echo 'Multiple linkerd paths specified:' >&2
           echo "  $linkerd_path" >&2
           echo "  $1" >&2
           cleanup_usage "$0" >&2
           exit 64
         fi
-        linkerd_path="$1"
+        linkerd_path=$1
         shift
         ;;
     esac
   done
 
   if [ -z "$linkerd_path" ]; then
-    echo "Error: path to linkerd binary is required" >&2
+    echo 'Error: path to linkerd binary is required' >&2
     cleanup_usage "$0" >&2
     exit 64
   fi
@@ -208,7 +208,7 @@ check_linkerd_binary() {
   exit_code=0
   "$linkerd_path" version --client > /dev/null 2>&1
   exit_on_err 'error running linkerd version command'
-  printf '[ok]\n'
+  echo '[ok]'
 }
 
 ##### Cluster helpers #####
@@ -236,7 +236,7 @@ setup_min_cluster() {
 
   test_setup
   if [ -z "$skip_cluster_create" ]; then
-    "$bindir"/k3d cluster create "$@"  --image +v1.20
+    "$bindir"/k3d cluster create "$@" --image +v1.20
     image_load "$name"
   fi
   check_cluster
@@ -270,14 +270,14 @@ check_if_k8s_reachable() {
   exit_code=0
   kubectl --context="$context" --request-timeout=5s get ns > /dev/null 2>&1
   exit_on_err 'error connecting to Kubernetes cluster'
-  printf '[ok]\n'
+  echo '[ok]'
 }
 
 check_if_l5d_exists() {
   printf 'Checking if Linkerd resources exist on cluster...'
   local resources
   resources=$(kubectl --context="$context" get all,clusterrole,clusterrolebinding,mutatingwebhookconfigurations,validatingwebhookconfigurations,crd -l linkerd.io/control-plane-ns --all-namespaces -oname)
-  if [ -n "$resources" ]; then
+  if [ "$resources" ]; then
     printf '
 Linkerd resources exist on cluster:
 \n%s\n
@@ -285,7 +285,7 @@ Help:
     Run: [%s/test-cleanup] ' "$resources" "$linkerd_path"
     exit 1
   fi
-  printf '[ok]\n'
+  echo '[ok]'
 }
 
 ##### Test runner helpers #####
@@ -334,7 +334,7 @@ start_test() {
       ;;
   esac
 
-  if [ "$name" == "multicluster" ]; then
+  if [ "$name" = 'multicluster' ]; then
     start_multicluster_test "${config[@]}"
   else
     start_single_test "${config[@]}"
@@ -343,12 +343,12 @@ start_test() {
 
 start_single_test() {
   name=$1
-  if [ "$name" == "helm-deep" ]; then
+  if [ "$name" = 'helm-deep' ]; then
     setup_min_cluster "$@"
   else
     setup_cluster "$@"
   fi
-  if [ -n "$cleanup_docker" ]; then
+  if [ "$cleanup_docker" ]; then
     rm -rf image-archives
     docker system prune --force --all
   fi
@@ -360,15 +360,15 @@ start_single_test() {
 start_multicluster_test() {
   setup_cluster source "$@"
   setup_cluster target "$@"
-  if [ -n "$cleanup_docker" ]; then
+  if [ "$cleanup_docker" ]; then
     rm -rf image-archives
     docker system prune --force --all
   fi
   run_multicluster_test
   exit_on_err "error calling 'run_multicluster_test'"
-  export context="k3d-source"
+  export context='k3d-source'
   finish source
-  export context="k3d-target"
+  export context='k3d-target'
   finish target
 }
 
@@ -408,7 +408,7 @@ setup_helm() {
   helm_charts="$( cd "$bindir"/.. && pwd )"/charts
   export helm_charts
   export helm_release_name='helm-test'
-  export helm_multicluster_release_name="multicluster-test"
+  export helm_multicluster_release_name='multicluster-test'
   "$bindir"/helm-build
   "$helm_path" --kube-context="$context" repo add linkerd https://helm.linkerd.io/stable
   exit_on_err 'error setting up Helm'
@@ -433,7 +433,7 @@ helm_cleanup() {
 
 run_helm-upgrade_test() {
   local stable_version
-  stable_version=$(latest_release_channel "stable")
+  stable_version=$(latest_release_channel 'stable')
 
   if [ -z "$stable_version" ]; then
     echo 'error getting stable_version'
@@ -484,7 +484,7 @@ exit_on_err() {
   exit_code=$?
   if [ $exit_code -ne 0 ]; then
     export GH_ANNOTATION=${GH_ANNOTATION:-}
-    if [ -n "$GH_ANNOTATION" ]; then
+    if [ "$GH_ANNOTATION" ]; then
       printf '::error::%s\n' "$1"
     else
       printf '\n=== FAIL: %s\n' "$1"
diff --git a/bin/test-cleanup b/bin/test-cleanup
index e70464cf9..5abaa88c1 100755
--- a/bin/test-cleanup
+++ b/bin/test-cleanup
@@ -19,10 +19,10 @@ echo "cleaning up multicluster resources, if present [${k8s_context}]"
 echo "cleaning up jaeger extension resources, if present [${k8s_context}]"
 "$linkerd_path" jaeger uninstall 2> /dev/null | kubectl --context="$k8s_context" delete -f -
 
-echo "cleaning up the all namespaces labelled with test.linkerd.io/is-test-data-plane"
+echo 'cleaning up the all namespaces labelled with test.linkerd.io/is-test-data-plane'
 kubectl --context="$k8s_context" delete ns -l test.linkerd.io/is-test-data-plane
 
-echo "cleaning up cluster-scoped resources labelled with test.linkerd.io/is-test-data-plane"
+echo 'cleaning up cluster-scoped resources labelled with test.linkerd.io/is-test-data-plane'
 kubectl --context="$k8s_context" delete clusterRole,clusterRoleBindings,mutatingwebhookconfiguration -l test.linkerd.io/is-test-data-plane
 
 echo "cleaning up linkerd resources [${k8s_context}]"
diff --git a/bin/test-clouds b/bin/test-clouds
index 630d2f4e7..77cbe1465 100755
--- a/bin/test-clouds
+++ b/bin/test-clouds
@@ -43,7 +43,7 @@ check_linkerd_binary() {
         printf '\nFailed to run linkerd version command\n'
         exit $exit_code
     fi
-    printf '[ok]\n'
+    echo '[ok]'
 }
 
 if [ "$#" -ne 1 ]; then
diff --git a/bin/test-clouds-cleanup b/bin/test-clouds-cleanup
index 2b6094d08..e5be1905d 100755
--- a/bin/test-clouds-cleanup
+++ b/bin/test-clouds-cleanup
@@ -14,9 +14,9 @@
 set -e
 
 if [ $# -ne 1 ]; then
-  echo "Error: accepts 1 argument"
-  echo "Usage:"
-  echo "  ${0##*/} /path/to/linkerd"
+  echo "Error: accepts 1 argument
+Usage:
+  ${0##*/} /path/to/linkerd"
   exit 1
 fi
 
diff --git a/bin/test-scale b/bin/test-scale
index 3a2c78718..99f95de3d 100755
--- a/bin/test-scale
+++ b/bin/test-scale
@@ -39,7 +39,7 @@ check_linkerd_binary(){
         printf '\nFailed to run linkerd version command\n'
         exit $exit_code
     fi
-    printf '[ok]\n'
+    echo '[ok]'
 }
 
 check_if_k8s_reachable(){
@@ -47,7 +47,8 @@ check_if_k8s_reachable(){
     exit_code=0
     kubectl --request-timeout=5s get ns > /dev/null 2>&1 || exit_code=$?
     if [ $exit_code -ne 0 ]; then
-        printf '\nFailed to connect to Kubernetes cluster\n'
+        echo '
+Failed to connect to Kubernetes cluster'
         exit $exit_code
     fi
     printf '[ok]\n'
diff --git a/bin/tests b/bin/tests
index 1114156fb..173248f59 100755
--- a/bin/tests
+++ b/bin/tests
@@ -6,20 +6,23 @@ bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
 . "$bindir"/_test-helpers.sh
 handle_tests_input "$@"
 
-if [ -n "$test_name" ]; then
+if [ "$test_name" ]; then
   start_test "$test_name"
 else
-  printf '==================RUNNING ALL TESTS==================
-Note: cluster-domain, cni-calico-deep and multicluster require a specific cluster configuration and are skipped by default\n\n'
+  echo '==================RUNNING ALL TESTS==================
+Note: cluster-domain, cni-calico-deep and multicluster require a specific cluster configuration and are skipped by default
+'
 
   for test_name in "${default_test_names[@]}"; do
     start_test "$test_name"
   done
 
   if [ $exit_code -eq 0 ]; then
-  printf '\n=== PASS: all tests passed\n'
+    echo '
+=== PASS: all tests passed'
   else
-    printf '\n=== FAIL: at least one test failed\n'
+    echo '
+=== FAIL: at least one test failed'
   fi
 
   exit $exit_code
diff --git a/pkg/profiles/proto_test.go b/pkg/profiles/proto_test.go
index 5728377c5..615e55bfc 100644
--- a/pkg/profiles/proto_test.go
+++ b/pkg/profiles/proto_test.go
@@ -10,54 +10,215 @@ import (
 )
 
 func TestProtoToServiceProfile(t *testing.T) {
-	namespace := "myns"
-	name := "mysvc"
-	clusterDomain := "mycluster.local"
+	var (
+		namespace     = "myns"
+		name          = "mysvc"
+		clusterDomain = "mycluster.local"
+	)
 
-	protobuf := `syntax = "proto3";
+	t.Run("rpc", func(t *testing.T) {
+		protobuf := `syntax = "proto3";
+	
+	package emojivoto.v1;
+	
+	message VoteRequest {
+	}
+	
+	message VotingResult {
+		string Shortcode = 1;
+		int32 Votes = 2;
+	}
+	
+	service VotingService {
+		rpc VotePoop (VoteRequest) returns (VoteResponse);
+	}`
 
-package emojivoto.v1;
+		parser := proto.NewParser(strings.NewReader(protobuf))
 
-message VoteRequest {
-}
+		expectedServiceProfile := sp.ServiceProfile{
+			TypeMeta: ServiceProfileMeta,
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      name + "." + namespace + ".svc." + clusterDomain,
+				Namespace: namespace,
+			},
+			Spec: sp.ServiceProfileSpec{
+				Routes: []*sp.RouteSpec{
+					{
+						Name: "VotePoop",
+						Condition: &sp.RequestMatch{
+							PathRegex: `/emojivoto\.v1\.VotingService/VotePoop`,
+							Method:    "POST",
+						},
+						IsRetryable: false,
+					},
+				},
+			},
+		}
 
-message VotingResult {
-    string Shortcode = 1;
-    int32 Votes = 2;
-}
+		actualServiceProfile, err := protoToServiceProfile(parser, namespace, name, clusterDomain)
+		if err != nil {
+			t.Fatalf("Failed to create ServiceProfile: %v", err)
+		}
+
+		err = ServiceProfileYamlEquals(*actualServiceProfile, expectedServiceProfile)
+		if err != nil {
+			t.Fatalf("ServiceProfiles are not equal: %v", err)
+		}
+	})
 
-service VotingService {
-	rpc VotePoop (VoteRequest) returns (VoteResponse);
-}`
-
-	parser := proto.NewParser(strings.NewReader(protobuf))
-
-	expectedServiceProfile := sp.ServiceProfile{
-		TypeMeta: ServiceProfileMeta,
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      name + "." + namespace + ".svc." + clusterDomain,
-			Namespace: namespace,
-		},
-		Spec: sp.ServiceProfileSpec{
-			Routes: []*sp.RouteSpec{
-				{
-					Name: "VotePoop",
-					Condition: &sp.RequestMatch{
-						PathRegex: `/emojivoto\.v1\.VotingService/VotePoop`,
-						Method:    "POST",
+	t.Run("rpc unknown idempotency level", func(t *testing.T) {
+		protobuf := `syntax = "proto3";
+	
+	package emojivoto.v1;
+	
+	message VoteRequest {
+	}
+	
+	message VotingResult {
+		string Shortcode = 1;
+		int32 Votes = 2;
+	}
+	
+	service VotingService {
+		rpc VotePoop (VoteRequest) returns (VoteResponse){
+			option idempotency_level = "UNKNOWN";
+		};
+	}`
+
+		parser := proto.NewParser(strings.NewReader(protobuf))
+
+		expectedServiceProfile := sp.ServiceProfile{
+			TypeMeta: ServiceProfileMeta,
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      name + "." + namespace + ".svc." + clusterDomain,
+				Namespace: namespace,
+			},
+			Spec: sp.ServiceProfileSpec{
+				Routes: []*sp.RouteSpec{
+					{
+						Name: "VotePoop",
+						Condition: &sp.RequestMatch{
+							PathRegex: `/emojivoto\.v1\.VotingService/VotePoop`,
+							Method:    "POST",
+						},
+						IsRetryable: false,
 					},
 				},
 			},
-		},
-	}
+		}
+
+		actualServiceProfile, err := protoToServiceProfile(parser, namespace, name, clusterDomain)
+		if err != nil {
+			t.Fatalf("Failed to create ServiceProfile: %v", err)
+		}
 
-	actualServiceProfile, err := protoToServiceProfile(parser, namespace, name, clusterDomain)
-	if err != nil {
-		t.Fatalf("Failed to create ServiceProfile: %v", err)
+		err = ServiceProfileYamlEquals(*actualServiceProfile, expectedServiceProfile)
+		if err != nil {
+			t.Fatalf("ServiceProfiles are not equal: %v", err)
+		}
+	})
+
+	t.Run("rpc idempotent", func(t *testing.T) {
+		protobuf := `syntax = "proto3";
+	
+	package emojivoto.v1;
+	
+	message VoteRequest {
+	}
+	
+	message VotingResult {
+		string Shortcode = 1;
+		int32 Votes = 2;
 	}
+	
+	service VotingService {
+		rpc VotePoop (VoteRequest) returns (VoteResponse){
+			option idempotency_level = "IDEMPOTENT";
+		};
+	}`
 
-	err = ServiceProfileYamlEquals(*actualServiceProfile, expectedServiceProfile)
-	if err != nil {
-		t.Fatalf("ServiceProfiles are not equal: %v", err)
+		parser := proto.NewParser(strings.NewReader(protobuf))
+
+		expectedServiceProfile := sp.ServiceProfile{
+			TypeMeta: ServiceProfileMeta,
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      name + "." + namespace + ".svc." + clusterDomain,
+				Namespace: namespace,
+			},
+			Spec: sp.ServiceProfileSpec{
+				Routes: []*sp.RouteSpec{
+					{
+						Name: "VotePoop",
+						Condition: &sp.RequestMatch{
+							PathRegex: `/emojivoto\.v1\.VotingService/VotePoop`,
+							Method:    "POST",
+						},
+						IsRetryable: true,
+					},
+				},
+			},
+		}
+
+		actualServiceProfile, err := protoToServiceProfile(parser, namespace, name, clusterDomain)
+		if err != nil {
+			t.Fatalf("Failed to create ServiceProfile: %v", err)
+		}
+
+		err = ServiceProfileYamlEquals(*actualServiceProfile, expectedServiceProfile)
+		if err != nil {
+			t.Fatalf("ServiceProfiles are not equal: %v", err)
+		}
+	})
+
+	t.Run("rpc no side effects", func(t *testing.T) {
+		protobuf := `syntax = "proto3";
+	
+	package emojivoto.v1;
+	
+	message VoteRequest {
+	}
+	
+	message VotingResult {
+		string Shortcode = 1;
+		int32 Votes = 2;
 	}
+	
+	service VotingService {
+		rpc VotePoop (VoteRequest) returns (VoteResponse){
+			option (idempotency_level) = "NO_SIDE_EFFECTS";
+		};
+	}`
+
+		parser := proto.NewParser(strings.NewReader(protobuf))
+
+		expectedServiceProfile := sp.ServiceProfile{
+			TypeMeta: ServiceProfileMeta,
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      name + "." + namespace + ".svc." + clusterDomain,
+				Namespace: namespace,
+			},
+			Spec: sp.ServiceProfileSpec{
+				Routes: []*sp.RouteSpec{
+					{
+						Name: "VotePoop",
+						Condition: &sp.RequestMatch{
+							PathRegex: `/emojivoto\.v1\.VotingService/VotePoop`,
+							Method:    "POST",
+						},
+						IsRetryable: true,
+					},
+				},
+			},
+		}
+
+		actualServiceProfile, err := protoToServiceProfile(parser, namespace, name, clusterDomain)
+		if err != nil {
+			t.Fatalf("Failed to create ServiceProfile: %v", err)
+		}
+
+		err = ServiceProfileYamlEquals(*actualServiceProfile, expectedServiceProfile)
+		if err != nil {
+			t.Fatalf("ServiceProfiles are not equal: %v", err)
+		}
+	})
 }