Repository: linkerd/linkerd2
Pull request #14206 (2025-07-03 22:25:16)

refactor(helm): Replace VFS with embed for Helm chart rendering
Replace the static FS generated by the vfs package with the go embed directive.  These are the same in concept, except that the go embed directive is performed automatically by the go build system at build time instead of requiring a separate `go generate` step.  Therefore this allows us to remove our use of `go generate` and avoids needing to manage generated sources.

<!--  Thanks for sending a pull request!

If you already have a well-structured git commit message, chances are GitHub
set the title and description of this PR to the git commit message subject and
body, respectively. If so, you may delete these instructions and submit your PR.

If this is your first time, please read our contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md

The title and description of your Pull Request should match the git commit
subject and body, respectively. Git commit messages are structured as follows:

```
Subject

Problem

Solution

Validation

Fixes #[GitHub issue ID]

DCO Sign off
```

Example git commit message:

```
Introduce Pull Request Template

GitHub's community guidelines recommend a pull request template, the repo was
lacking one.

Introduce a `PULL_REQUEST_TEMPLATE.md` file.

Once merged, the
[Community profile checklist](https://github.com/linkerd/linkerd2/community)
should indicate the repo now provides a pull request template.

Fixes #3321

Signed-off-by: Jane Smith <jane.smith@example.com>
```

Note the git commit message subject becomes the pull request title.

For more details around git commits, see the section on Committing in our
contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md#committing
-->



Patch:
diff --git a/.github/workflows/choco.yml b/.github/workflows/choco.yml
deleted file mode 100644
index 5cc084803..000000000
--- a/.github/workflows/choco.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-name: Choco
-
-on:
-  pull_request:
-    paths:
-      - bin/win/**
-
-permissions:
-  contents: read
-
-jobs:
-  choco-psscript-lint:
-    timeout-minutes: 10
-    runs-on: ubuntu-24.04
-    steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
-      - uses: devblackops/github-action-psscriptanalyzer@854038567344559afaaa8ccb7a014452b99d86ee
-        env:
-          # https://github.com/devblackops/github-action-psscriptanalyzer/pull/3/files
-          INPUT_FAILONWARNING: 1
-        with:
-          rootPath: bin/win/tools
-          failOnInfos: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5c4bedec8..64e579ce9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -148,41 +148,11 @@ jobs:
           [[ "$TAG" == "$($CMD version --short --client)" ]]
           bin/tests --images preload --name ${{ matrix.integration_test }} "$CMD"
 
-  choco_pack:
-    # only runs for stable tags. The conditionals are at each step level instead of the job level
-    # otherwise the jobs below that depend on this one won't run
-    name: Pack Chocolatey release
-    timeout-minutes: 30
-    needs: [integration_tests]
-    runs-on: windows-2019
-    steps:
-      - name: Checkout code
-        if: startsWith(github.ref, 'refs/tags/stable')
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
-      - name: Chocolatey - update nuspec
-        if: startsWith(github.ref, 'refs/tags/stable')
-        run: |
-          $LINKERD_VERSION=$env:GITHUB_REF.Substring(17)
-          (Get-Content bin\win\linkerd.nuspec).replace('LINKERD_VERSION', "$LINKERD_VERSION") | Set-Content bin\win\linkerd.nuspec
-      - name: Chocolatey - pack
-        if: startsWith(github.ref, 'refs/tags/stable')
-        uses: crazy-max/ghaction-chocolatey@2526f467ccbd337d307fe179959cabbeca0bc8c0
-        with:
-          args: pack bin/win/linkerd.nuspec
-      - name: Chocolatey - upload package
-        if: startsWith(github.ref, 'refs/tags/stable')
-        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
-        with:
-          name: choco
-          path: ./linkerd.*.nupkg
-
   gh_release:
     name: Create GH release
     needs:
       - tag
       - integration_tests
-    # TODO(ver) choco packages are not produced for edge releases...
-    # - choco_pack
     timeout-minutes: 30
     runs-on: ubuntu-24.04
     permissions:
diff --git a/.gitignore b/.gitignore
index 55ceac7c1..31b2b6d21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,7 +12,6 @@ web/app/js/locales/_build
 web/app/js/locales/**/*.js
 web/app/yarn-error.log
 vendor
-**/*.gogen*
 **/*.swp
 **/charts/**/charts
 package-lock.json
diff --git a/Cargo.lock b/Cargo.lock
index 4b836e4bd..1f9abb302 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2089,9 +2089,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.23.28"
+version = "0.23.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
+checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1"
 dependencies = [
  "log",
  "once_cell",
@@ -2147,9 +2147,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-webpki"
-version = "0.103.3"
+version = "0.103.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
+checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
 dependencies = [
  "ring",
  "rustls-pki-types",
diff --git a/bin/build-cli-bin b/bin/build-cli-bin
index 892dc6648..44d9500ad 100755
--- a/bin/build-cli-bin
+++ b/bin/build-cli-bin
@@ -17,12 +17,7 @@ rootdir=$( cd "$bindir"/.. && pwd )
     cd "$rootdir"
     cd "$(pwd -P)"
     target=target/cli/$(os)/linkerd
-     # TODO: `go generate` does not honor -mod=readonly
-    GO111MODULE=on go generate -mod=readonly ./pkg/charts/static
-    GO111MODULE=on go generate -mod=readonly ./jaeger/static
-    GO111MODULE=on go generate -mod=readonly ./multicluster/static
-    GO111MODULE=on go generate -mod=readonly ./viz/static
-    
+
     root_tag=$("$bindir"/root-tag)
     GO111MODULE=on CGO_ENABLED=0 go build -o "$target" -tags prod -mod=readonly -ldflags "-s -w -X github.com/linkerd/linkerd2/pkg/version.Version=$root_tag" ./cli
     echo "$target"
diff --git a/bin/win/linkerd.nuspec b/bin/win/linkerd.nuspec
deleted file mode 100644
index 0bb9b1b15..000000000
--- a/bin/win/linkerd.nuspec
+++ /dev/null
@@ -1,32 +0,0 @@
-﻿<?xml version="1.0" encoding="utf-8"?>
-<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
-  <metadata>
-    <id>linkerd</id>
-    <version>LINKERD_VERSION</version>
-    
-    <title>Linkerd</title>
-    <authors>Linkerd</authors>
-    <summary>Ultralight service mesh for Kubernetes and beyond</summary>
-    <description>
-    Linkerd is an ultralight service mesh for Kubernetes. 
-    It gives you observability, reliability, and security without requiring any code changes.
-
-    Package Parameters
-    - `path`: Specify path for linkerd-cli installation. If not specified, uses environment variable `linkerdPath`. Uses default chocolatey install path if none provided. 
-    - `checksum`: Specify the checksum for linkerd-cli. If not specified, uses environment variable `linkerdCheckSum`. Defaults to `null` if no checksum provided and skips verification. Do ensure to use the correct checksum for the version of install. 
-    </description>
-
-    <projectUrl>https://github.com/linkerd/linkerd2</projectUrl>
-    <licenseUrl>https://github.com/linkerd/linkerd2/blob/main/LICENSE</licenseUrl>
-    <releaseNotes>https://github.com/linkerd/linkerd2/releases</releaseNotes>
-    <docsUrl>https://linkerd.io/docs</docsUrl>
-
-    <dependencies>
-      <dependency id="chocolatey-core.extension" version="1.1.0" />
-    </dependencies>
-  </metadata>
-
-  <files>
-    <file src="tools\**" target="tools" />
-    </files>
-</package>
diff --git a/bin/win/tools/chocolateyinstall.ps1 b/bin/win/tools/chocolateyinstall.ps1
deleted file mode 100644
index 6c05e3924..000000000
--- a/bin/win/tools/chocolateyinstall.ps1
+++ /dev/null
@@ -1,33 +0,0 @@
-﻿$ErrorActionPreference = 'Stop';
-$toolsPath = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
-
-$version = $env:chocolateyPackageVersion
-$pp = Get-PackageParameters
-
-if ($null -ne $pp['path']){
-	$lpath = $pp['path']
-}
-elseif (Test-Path env:linkerdPath) {
-	$lpath = $env:linkerdPath
-}
-else {
-	$lpath = $toolsPath
-}
-
-if ($null -ne $pp['checksum']){
-	$checksum = $pp['checksum']
-}
-else{
-	$checksum = $env:linkerdCheckSum
-}
-
-$packageArgs = @{
-  packageName    = 'linkerd'
-  fileFullPath   = "$lpath\linkerd.exe"
-  url64          = "https://github.com/linkerd/linkerd2/releases/download/stable-$version/linkerd2-cli-stable-$version-windows.exe"
-  checksum       = $checksum
-  checksumType   = 'sha256'
-}
-
-Get-ChocolateyWebFile @packageArgs
-Install-ChocolateyPath $packageArgs.fileFullPath 'User'
diff --git a/charts/linkerd-control-plane/templates/destination.yaml b/charts/linkerd-control-plane/templates/destination.yaml
index 84bccdb49..2d9fe6989 100644
--- a/charts/linkerd-control-plane/templates/destination.yaml
+++ b/charts/linkerd-control-plane/templates/destination.yaml
@@ -180,7 +180,9 @@ spec:
       {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- $_ := set $tree "component" "destination" -}}
-      {{- include "linkerd.affinity" $tree | nindent 6 }}
+      {{- with include "linkerd.affinity" $tree }}
+      {{- . | nindent 6 }}
+      {{- end }}
       automountServiceAccountToken: false
       containers:
       {{- $_ := set $tree.Values.proxy "await" $tree.Values.proxy.await }}
diff --git a/charts/linkerd-control-plane/templates/identity.yaml b/charts/linkerd-control-plane/templates/identity.yaml
index fe80df929..2de0d3b6f 100644
--- a/charts/linkerd-control-plane/templates/identity.yaml
+++ b/charts/linkerd-control-plane/templates/identity.yaml
@@ -153,7 +153,9 @@ spec:
       {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- $_ := set $tree "component" "identity" -}}
-      {{- include "linkerd.affinity" $tree | nindent 6 }}
+      {{- with include "linkerd.affinity" $tree }}
+      {{- . | nindent 6 }}
+      {{- end }}
       automountServiceAccountToken: false
       containers:
       - args:
@@ -274,7 +276,7 @@ spec:
       - {{- include "partials.proxy.volumes.service-account-token" . | indent 8 | trimPrefix (repeat 7 " ") }}
       {{ end -}}
       - {{- include "partials.proxy.volumes.identity" . | indent 8 | trimPrefix (repeat 7 " ") }}
-      {{ if .Values.proxy.tracing.enable -}}
+      {{- if .Values.proxy.tracing.enable }}
       - {{- include "partials.proxy.volumes.podinfo" . | indent 8 | trimPrefix (repeat 7 " ") }}
-      {{ end }}
+      {{- end }}
 {{end -}}
diff --git a/charts/linkerd-control-plane/templates/proxy-injector.yaml b/charts/linkerd-control-plane/templates/proxy-injector.yaml
index 280657296..25bd0d5ca 100644
--- a/charts/linkerd-control-plane/templates/proxy-injector.yaml
+++ b/charts/linkerd-control-plane/templates/proxy-injector.yaml
@@ -60,7 +60,9 @@ spec:
       {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- $_ := set $tree "component" "proxy-injector" -}}
-      {{- include "linkerd.affinity" $tree | nindent 6 }}
+      {{- with include "linkerd.affinity" $tree }}
+      {{- . | nindent 6 }}
+      {{- end }}
       automountServiceAccountToken: false
       containers:
       {{- $_ := set $tree.Values.proxy "await" $tree.Values.proxy.await }}
diff --git a/charts/partials/templates/_proxy.tpl b/charts/partials/templates/_proxy.tpl
index 119eea0c9..f07e2cbec 100644
--- a/charts/partials/templates/_proxy.tpl
+++ b/charts/partials/templates/_proxy.tpl
@@ -170,7 +170,7 @@ env:
     k8s.pod.uid=$(_pod_uid)
     k8s.container.name=$(_pod_containerName)
 {{ end -}}
-{{- /* Configure inbound and outbound parameters, e.g. for HTTP/2 servers. */}}
+{{/* Configure inbound and outbound parameters, e.g. for HTTP/2 servers. */ -}}
 {{ range $proxyK, $proxyV := (dict "inbound" .Values.proxy.inbound "outbound" .Values.proxy.outbound) -}}
 {{   range $scopeK, $scopeV := $proxyV -}}
 {{     range $protoK, $protoV := $scopeV -}}
diff --git a/charts/templates.go b/charts/templates.go
new file mode 100644
index 000000000..3e5761bbc
--- /dev/null
+++ b/charts/templates.go
@@ -0,0 +1,8 @@
+package charts
+
+import (
+	"embed"
+)
+
+//go:embed linkerd-control-plane linkerd-crds linkerd2-cni all:partials patch
+var Templates embed.FS
diff --git a/cli/Dockerfile b/cli/Dockerfile
index c81073702..e2077eed2 100644
--- a/cli/Dockerfile
+++ b/cli/Dockerfile
@@ -22,13 +22,6 @@ COPY controller/api controller/api
 COPY controller/gen controller/gen
 COPY pkg pkg
 
-# Generate static templates
-# TODO: `go generate` does not honor -mod=readonly
-RUN go generate -mod=readonly ./pkg/charts/static
-RUN go generate -mod=readonly ./jaeger/static
-RUN go generate -mod=readonly ./multicluster/static
-RUN go generate -mod=readonly ./viz/static
-
 RUN mkdir -p /out
 
 FROM go-gen AS linux-amd64-full
diff --git a/cli/cmd/install-cni-plugin.go b/cli/cmd/install-cni-plugin.go
index 1b1906e60..eb8e19698 100644
--- a/cli/cmd/install-cni-plugin.go
+++ b/cli/cmd/install-cni-plugin.go
@@ -6,9 +6,9 @@ import (
 	"os"
 	"strings"
 
-	"github.com/linkerd/linkerd2/pkg/charts"
+	"github.com/linkerd/linkerd2/charts"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	cnicharts "github.com/linkerd/linkerd2/pkg/charts/cni"
-	"github.com/linkerd/linkerd2/pkg/charts/static"
 	"github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/version"
@@ -249,13 +249,13 @@ func renderCNIPlugin(w io.Writer, valOpts values.Options, config *cniPluginOptio
 		{Name: "templates/cni-plugin.yaml"},
 	}
 
-	ch := &charts.Chart{
+	ch := &chartspkg.Chart{
 		Name:      helmCNIDefaultChartName,
 		Dir:       helmCNIDefaultChartDir,
 		Namespace: defaultCNINamespace,
 		Values:    mergedValues,
 		Files:     files,
-		Fs:        static.Templates,
+		Fs:        charts.Templates,
 	}
 
 	buf, err := ch.RenderCNI()
diff --git a/cli/cmd/install.go b/cli/cmd/install.go
index 7060b954d..1889368d5 100644
--- a/cli/cmd/install.go
+++ b/cli/cmd/install.go
@@ -13,10 +13,10 @@ import (
 	"text/template"
 	"time"
 
+	"github.com/linkerd/linkerd2/charts"
 	"github.com/linkerd/linkerd2/cli/flag"
-	"github.com/linkerd/linkerd2/pkg/charts"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
-	"github.com/linkerd/linkerd2/pkg/charts/static"
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	flagspkg "github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
@@ -331,14 +331,11 @@ func isRunAsRoot(values map[string]interface{}) bool {
 // them into a buffer. The coalesced values are also returned so that they may be rendered via
 // `renderOverrides` if appropriate.
 func renderChartToBuffer(files []*loader.BufferedFile, values map[string]interface{}, valuesOverrides map[string]interface{}) (*bytes.Buffer, chartutil.Values, error) {
-	// Load the partials in addition to the main chart.
-	var partials []*loader.BufferedFile
-	for _, template := range charts.L5dPartials {
-		partials = append(partials, &loader.BufferedFile{Name: template})
-	}
-	if err := charts.FilesReader(static.Templates, "", partials); err != nil {
+	partials, err := chartspkg.LoadPartials()
+	if err != nil {
 		return nil, nil, err
 	}
+
 	chart, err := loader.LoadFiles(append(files, partials...))
 	if err != nil {
 		return nil, nil, err
@@ -435,13 +432,13 @@ func renderCRDs(ctx context.Context, k *k8s.KubernetesAPI, w io.Writer, options
 	for _, template := range TemplatesCrdFiles {
 		files = append(files, &loader.BufferedFile{Name: template})
 	}
-	if err := charts.FilesReader(static.Templates, l5dcharts.HelmChartDirCrds+"/", files); err != nil {
+	if err := chartspkg.FilesReader(charts.Templates, l5dcharts.HelmChartDirCrds+"/", files); err != nil {
 		return err
 	}
 
 	// Load defaults from values.yaml
 	valuesFile := &loader.BufferedFile{Name: l5dcharts.HelmChartDirCrds + "/values.yaml"}
-	if err := charts.ReadFile(static.Templates, "/", valuesFile); err != nil {
+	if err := chartspkg.ReadFile(charts.Templates, "", valuesFile); err != nil {
 		return err
 	}
 	// Ensure the map is not nil, even if the default `values.yaml` is empty ---
@@ -470,7 +467,7 @@ func renderCRDs(ctx context.Context, k *k8s.KubernetesAPI, w io.Writer, options
 		}
 
 		defaultValues = updateDefaultValues(installed, defaultValues)
-		finalValues := charts.MergeMaps(defaultValues, valuesOverrides)
+		finalValues := chartspkg.MergeMaps(defaultValues, valuesOverrides)
 
 		if err := validateFinalValues(installed, finalValues); err != nil {
 			return err
@@ -492,7 +489,7 @@ func renderControlPlane(w io.Writer, values *l5dcharts.Values, valuesOverrides m
 	for _, template := range TemplatesControlPlane {
 		files = append(files, &loader.BufferedFile{Name: template})
 	}
-	if err := charts.FilesReader(static.Templates, l5dcharts.HelmChartDirCP+"/", files); err != nil {
+	if err := chartspkg.FilesReader(charts.Templates, l5dcharts.HelmChartDirCP+"/", files); err != nil {
 		return err
 	}
 
diff --git a/controller/Dockerfile b/controller/Dockerfile
index 283bd8969..986277143 100644
--- a/controller/Dockerfile
+++ b/controller/Dockerfile
@@ -14,14 +14,12 @@ FROM go-deps AS golang
 WORKDIR /linkerd-build
 COPY controller/gen controller/gen
 COPY pkg pkg
+COPY charts charts
 COPY controller controller
 COPY charts/patch charts/patch
 COPY charts/partials charts/partials
 COPY multicluster multicluster
 
-# Generate static templates
-# TODO: `go generate` does not honor -mod=readonly
-RUN go generate -mod=readonly ./pkg/charts/static
 ARG TARGETARCH
 RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build -o /out/controller -tags prod -mod=readonly -ldflags "-s -w" ./controller/cmd
 
diff --git a/go.mod b/go.mod
index 635efa889..710f6d432 100644
--- a/go.mod
+++ b/go.mod
@@ -35,7 +35,7 @@ require (
 	github.com/spf13/cobra v1.9.1
 	github.com/spf13/pflag v1.0.6
 	go.opencensus.io v0.24.0
-	golang.org/x/net v0.41.0
+	golang.org/x/net v0.42.0
 	golang.org/x/tools v0.34.0
 	google.golang.org/grpc v1.73.0
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
@@ -134,13 +134,13 @@ require (
 	go.opentelemetry.io/otel/trace v1.35.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	go.yaml.in/yaml/v3 v3.0.3 // indirect
-	golang.org/x/crypto v0.39.0 // indirect
+	golang.org/x/crypto v0.40.0 // indirect
 	golang.org/x/mod v0.25.0 // indirect
 	golang.org/x/oauth2 v0.30.0 // indirect
-	golang.org/x/sync v0.15.0 // indirect
-	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/term v0.32.0 // indirect
-	golang.org/x/text v0.26.0 // indirect
+	golang.org/x/sync v0.16.0 // indirect
+	golang.org/x/sys v0.34.0 // indirect
+	golang.org/x/term v0.33.0 // indirect
+	golang.org/x/text v0.27.0 // indirect
 	golang.org/x/time v0.9.0 // indirect
 	google.golang.org/api v0.143.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
diff --git a/go.sum b/go.sum
index 1d57a26ca..bec3c54e2 100644
--- a/go.sum
+++ b/go.sum
@@ -460,8 +460,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
-golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
+golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
+golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -515,8 +515,8 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
-golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
+golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
+golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -532,8 +532,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
-golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -559,17 +559,17 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
-golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
-golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
+golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
+golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
+golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
-golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
+golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/jaeger/charts/templates.go b/jaeger/charts/templates.go
new file mode 100644
index 000000000..7a24a2f31
--- /dev/null
+++ b/jaeger/charts/templates.go
@@ -0,0 +1,8 @@
+package charts
+
+import (
+	"embed"
+)
+
+//go:embed linkerd-jaeger
+var Templates embed.FS
diff --git a/jaeger/cmd/install.go b/jaeger/cmd/install.go
index 58f05dc75..d310f5a1e 100644
--- a/jaeger/cmd/install.go
+++ b/jaeger/cmd/install.go
@@ -8,9 +8,8 @@ import (
 	"path"
 	"time"
 
-	"github.com/linkerd/linkerd2/jaeger/static"
-	"github.com/linkerd/linkerd2/pkg/charts"
-	partials "github.com/linkerd/linkerd2/pkg/charts/static"
+	"github.com/linkerd/linkerd2/jaeger/charts"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
@@ -126,20 +125,13 @@ func render(w io.Writer, valuesOverrides map[string]interface{}, registry string
 		)
 	}
 
-	var partialFiles []*loader.BufferedFile
-	for _, template := range charts.L5dPartials {
-		partialFiles = append(partialFiles,
-			&loader.BufferedFile{Name: template},
-		)
-	}
-
 	// Load all jaeger chart files into buffer
-	if err := charts.FilesReader(static.Templates, "linkerd-jaeger/", files); err != nil {
+	if err := chartspkg.FilesReader(charts.Templates, "linkerd-jaeger/", files); err != nil {
 		return err
 	}
 
-	// Load all partial chart files into buffer
-	if err := charts.FilesReader(partials.Templates, "", partialFiles); err != nil {
+	partialFiles, err := chartspkg.LoadPartials()
+	if err != nil {
 		return err
 	}
 
@@ -154,7 +146,7 @@ func render(w io.Writer, valuesOverrides map[string]interface{}, registry string
 		return err
 	}
 
-	vals, err = charts.InsertVersionValues(vals)
+	vals, err = chartspkg.InsertVersionValues(vals)
 	if err != nil {
 		return err
 	}
diff --git a/jaeger/static/generate.go b/jaeger/static/generate.go
deleted file mode 100644
index cb0637209..000000000
--- a/jaeger/static/generate.go
+++ /dev/null
@@ -1,21 +0,0 @@
-//go:build ignore
-
-package main
-
-import (
-	"github.com/linkerd/linkerd2/jaeger/static"
-	"github.com/shurcooL/vfsgen"
-	log "github.com/sirupsen/logrus"
-)
-
-func main() {
-	err := vfsgen.Generate(static.Templates, vfsgen.Options{
-		Filename:     "generated_jaeger_templates.gogen.go",
-		PackageName:  "static",
-		BuildTags:    "prod",
-		VariableName: "Templates",
-	})
-	if err != nil {
-		log.Fatalln(err)
-	}
-}
diff --git a/jaeger/static/templates.go b/jaeger/static/templates.go
deleted file mode 100644
index a132ddda4..000000000
--- a/jaeger/static/templates.go
+++ /dev/null
@@ -1,15 +0,0 @@
-//go:generate go run generate.go
-//go:build !prod
-
-package static
-
-import (
-	"net/http"
-	"path"
-
-	"github.com/linkerd/linkerd2/pkg/charts/static"
-)
-
-// Templates that will be rendered by `linkerd install`. This is only used on
-// dev builds.
-var Templates http.FileSystem = http.Dir(path.Join(static.GetRepoRoot(), "jaeger/charts"))
diff --git a/multicluster/charts/linkerd-multicluster/templates/controller/deployment.yaml b/multicluster/charts/linkerd-multicluster/templates/controller/deployment.yaml
index 30024cd1b..2e88a5d22 100644
--- a/multicluster/charts/linkerd-multicluster/templates/controller/deployment.yaml
+++ b/multicluster/charts/linkerd-multicluster/templates/controller/deployment.yaml
@@ -40,7 +40,9 @@ spec:
     {{- $_ := set $tree.Values "nodeAffinity" (dig "nodeAffinity" $.Values.controllerDefaults.nodeAffinity .) -}}
     {{- $_ := set $tree "component" .link.ref.name -}}
     {{- $_ := set $tree "label" "mirror.linkerd.io/cluster-name" -}}
-    {{- include "linkerd.affinity" $tree | nindent 6 }}
+    {{- with include "linkerd.affinity" $tree }}
+    {{- . | nindent 6 }}
+    {{- end }}
       automountServiceAccountToken: false
       containers:
       - args:
diff --git a/multicluster/charts/linkerd-multicluster/templates/gateway.yaml b/multicluster/charts/linkerd-multicluster/templates/gateway.yaml
index 7c813f059..6262d4beb 100644
--- a/multicluster/charts/linkerd-multicluster/templates/gateway.yaml
+++ b/multicluster/charts/linkerd-multicluster/templates/gateway.yaml
@@ -44,7 +44,9 @@ spec:
     spec:
       {{- $_ := set $tree "component" .Values.gateway.name -}}
       {{- $_ := set $tree "label" "app" -}}
-      {{- include "linkerd.affinity" $tree | nindent 6 }}
+      {{- with include "linkerd.affinity" $tree }}
+      {{- . | nindent 6 }}
+      {{- end }}
       {{- if .Values.gateway.terminationGracePeriodSeconds }}
       terminationGracePeriodSeconds: {{.Values.gateway.terminationGracePeriodSeconds}}
       {{- end }}
diff --git a/multicluster/charts/templates.go b/multicluster/charts/templates.go
new file mode 100644
index 000000000..1c6ee6d1f
--- /dev/null
+++ b/multicluster/charts/templates.go
@@ -0,0 +1,8 @@
+package charts
+
+import (
+	"embed"
+)
+
+//go:embed linkerd-multicluster linkerd-multicluster-link
+var Templates embed.FS
diff --git a/multicluster/cmd/allow.go b/multicluster/cmd/allow.go
index 5d955c879..760593d42 100644
--- a/multicluster/cmd/allow.go
+++ b/multicluster/cmd/allow.go
@@ -5,9 +5,9 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/linkerd/linkerd2/multicluster/static"
+	"github.com/linkerd/linkerd2/multicluster/charts"
 	mccharts "github.com/linkerd/linkerd2/multicluster/values"
-	"github.com/linkerd/linkerd2/pkg/charts"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/version"
@@ -59,13 +59,13 @@ func newAllowCommand() *cobra.Command {
 				{Name: "templates/remote-access-service-mirror-rbac.yaml"},
 			}
 
-			chart := &charts.Chart{
-				Name:      helmMulticlusterDefaultChartName,
-				Dir:       helmMulticlusterDefaultChartName,
+			chart := &chartspkg.Chart{
+				Name:      mccharts.HelmDefaultChartDir,
+				Dir:       mccharts.HelmDefaultChartDir,
 				Namespace: opts.namespace,
 				RawValues: rawValues,
 				Files:     files,
-				Fs:        static.Templates,
+				Fs:        charts.Templates,
 			}
 			buf, err := chart.Render()
 			if err != nil {
diff --git a/multicluster/cmd/install.go b/multicluster/cmd/install.go
index 8365b4ccc..a90b0dc96 100644
--- a/multicluster/cmd/install.go
+++ b/multicluster/cmd/install.go
@@ -11,10 +11,9 @@ import (
 	"strings"
 	"time"
 
-	"github.com/linkerd/linkerd2/multicluster/static"
+	"github.com/linkerd/linkerd2/multicluster/charts"
 	multicluster "github.com/linkerd/linkerd2/multicluster/values"
-	"github.com/linkerd/linkerd2/pkg/charts"
-	partials "github.com/linkerd/linkerd2/pkg/charts/static"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
@@ -137,7 +136,7 @@ func install(ctx context.Context, w io.Writer, options *multiclusterInstallOptio
 	}
 
 	if ha {
-		valuesOverrides, err = charts.OverrideFromFile(valuesOverrides, static.Templates, helmMulticlusterDefaultChartName, "values-ha.yaml")
+		valuesOverrides, err = chartspkg.OverrideFromFile(valuesOverrides, charts.Templates, multicluster.HelmDefaultChartDir, "values-ha.yaml")
 		if err != nil {
 			return err
 		}
@@ -156,20 +155,13 @@ func render(w io.Writer, values *multicluster.Values, valuesOverrides map[string
 		files = append(files, &loader.BufferedFile{Name: template})
 	}
 
-	var partialFiles []*loader.BufferedFile
-	for _, template := range charts.L5dPartials {
-		partialFiles = append(partialFiles,
-			&loader.BufferedFile{Name: template},
-		)
-	}
-
 	// Load all multicluster install chart files into buffer
-	if err := charts.FilesReader(static.Templates, helmMulticlusterDefaultChartName+"/", files); err != nil {
+	if err := chartspkg.FilesReader(charts.Templates, multicluster.HelmDefaultChartDir+"/", files); err != nil {
 		return err
 	}
 
-	// Load all partial chart files into buffer
-	if err := charts.FilesReader(partials.Templates, "", partialFiles); err != nil {
+	partialFiles, err := chartspkg.LoadPartials()
+	if err != nil {
 		return err
 	}
 
diff --git a/multicluster/cmd/link.go b/multicluster/cmd/link.go
index 3511b39db..d21476504 100644
--- a/multicluster/cmd/link.go
+++ b/multicluster/cmd/link.go
@@ -10,10 +10,9 @@ import (
 	"strings"
 
 	"github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha3"
-	"github.com/linkerd/linkerd2/multicluster/static"
+	"github.com/linkerd/linkerd2/multicluster/charts"
 	multicluster "github.com/linkerd/linkerd2/multicluster/values"
-	"github.com/linkerd/linkerd2/pkg/charts"
-	partials "github.com/linkerd/linkerd2/pkg/charts/static"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/k8s"
@@ -346,10 +345,10 @@ A full list of configurable values can be found at https://github.com/linkerd/li
 			}
 
 			if opts.ha {
-				if valuesOverrides, err = charts.OverrideFromFile(
+				if valuesOverrides, err = chartspkg.OverrideFromFile(
 					valuesOverrides,
-					static.Templates,
-					helmMulticlusterLinkDefaultChartName,
+					charts.Templates,
+					multicluster.HelmDefaultLinkChartDir,
 					"values-ha.yaml",
 				); err != nil {
 					return err
@@ -415,20 +414,13 @@ func renderServiceMirror(values *multicluster.Values, valuesOverrides map[string
 		{Name: "templates/gateway-mirror.yaml"},
 	}
 
-	var partialFiles []*chartloader.BufferedFile
-	for _, template := range charts.L5dPartials {
-		partialFiles = append(partialFiles,
-			&chartloader.BufferedFile{Name: template},
-		)
-	}
-
 	// Load all multicluster link chart files into buffer
-	if err := charts.FilesReader(static.Templates, helmMulticlusterLinkDefaultChartName+"/", files); err != nil {
+	if err := chartspkg.FilesReader(charts.Templates, multicluster.HelmDefaultLinkChartDir+"/", files); err != nil {
 		return nil, err
 	}
 
-	// Load all partial chart files into buffer
-	if err := charts.FilesReader(partials.Templates, "", partialFiles); err != nil {
+	partialFiles, err := chartspkg.LoadPartials()
+	if err != nil {
 		return nil, err
 	}
 
diff --git a/multicluster/cmd/root.go b/multicluster/cmd/root.go
index a420c2842..a2d2e6930 100644
--- a/multicluster/cmd/root.go
+++ b/multicluster/cmd/root.go
@@ -17,7 +17,6 @@ const (
 	defaultLinkerdNamespace              = "linkerd"
 	defaultMulticlusterNamespace         = "linkerd-multicluster"
 	defaultGatewayName                   = "linkerd-gateway"
-	helmMulticlusterDefaultChartName     = "linkerd-multicluster"
 	helmMulticlusterLinkDefaultChartName = "linkerd-multicluster-link"
 	tokenKey                             = "token"
 
@@ -30,6 +29,8 @@ const (
 )
 
 var (
+	HelmMulticlusterDefaultChartName = "linkerd-multicluster"
+
 	apiAddr               string // An empty value means "use the Kubernetes configuration"
 	controlPlaneNamespace string
 	kubeconfigPath        string
diff --git a/multicluster/static/generate.go b/multicluster/static/generate.go
deleted file mode 100644
index cd8049a28..000000000
--- a/multicluster/static/generate.go
+++ /dev/null
@@ -1,21 +0,0 @@
-//go:build ignore
-
-package main
-
-import (
-	"github.com/linkerd/linkerd2/multicluster/static"
-	"github.com/shurcooL/vfsgen"
-	log "github.com/sirupsen/logrus"
-)
-
-func main() {
-	err := vfsgen.Generate(static.Templates, vfsgen.Options{
-		Filename:     "generated_multicluster_templates.gogen.go",
-		PackageName:  "static",
-		BuildTags:    "prod",
-		VariableName: "Templates",
-	})
-	if err != nil {
-		log.Fatalln(err)
-	}
-}
diff --git a/multicluster/static/templates.go b/multicluster/static/templates.go
deleted file mode 100644
index c85a765da..000000000
--- a/multicluster/static/templates.go
+++ /dev/null
@@ -1,15 +0,0 @@
-//go:generate go run generate.go
-//go:build !prod
-
-package static
-
-import (
-	"net/http"
-	"path"
-
-	"github.com/linkerd/linkerd2/pkg/charts/static"
-)
-
-// Templates that will be rendered by `linkerd install`. This is only used on
-// dev builds.
-var Templates http.FileSystem = http.Dir(path.Join(static.GetRepoRoot(), "multicluster/charts"))
diff --git a/multicluster/values/values.go b/multicluster/values/values.go
index 65cd8eabd..84aa6af79 100644
--- a/multicluster/values/values.go
+++ b/multicluster/values/values.go
@@ -3,8 +3,8 @@ package values
 import (
 	"fmt"
 
-	"github.com/linkerd/linkerd2/multicluster/static"
-	"github.com/linkerd/linkerd2/pkg/charts"
+	"github.com/linkerd/linkerd2/multicluster/charts"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	"github.com/linkerd/linkerd2/pkg/charts/linkerd2"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"helm.sh/helm/v3/pkg/chart/loader"
@@ -13,9 +13,9 @@ import (
 	"sigs.k8s.io/yaml"
 )
 
-const (
-	helmDefaultChartDir     = "linkerd-multicluster"
-	helmDefaultLinkChartDir = "linkerd-multicluster-link"
+var (
+	HelmDefaultChartDir     = "linkerd-multicluster"
+	HelmDefaultLinkChartDir = "linkerd-multicluster-link"
 )
 
 // Values contains the top-level elements in the Helm charts
@@ -113,7 +113,7 @@ type ControllerDefaultsProbe struct {
 
 // NewInstallValues returns a new instance of the Values type.
 func NewInstallValues() (*Values, error) {
-	chartDir := fmt.Sprintf("%s/", helmDefaultChartDir)
+	chartDir := fmt.Sprintf("%s/", HelmDefaultChartDir)
 	v, err := readDefaults(chartDir)
 	if err != nil {
 		return nil, err
@@ -125,7 +125,7 @@ func NewInstallValues() (*Values, error) {
 
 // NewLinkValues returns a new instance of the Values type.
 func NewLinkValues() (*Values, error) {
-	chartDir := fmt.Sprintf("%s/", helmDefaultLinkChartDir)
+	chartDir := fmt.Sprintf("%s/", HelmDefaultLinkChartDir)
 	v, err := readDefaults(chartDir)
 	if err != nil {
 		return nil, err
@@ -141,11 +141,11 @@ func readDefaults(chartDir string) (*Values, error) {
 	file := &loader.BufferedFile{
 		Name: chartutil.ValuesfileName,
 	}
-	if err := charts.ReadFile(static.Templates, chartDir, file); err != nil {
+	if err := chartspkg.ReadFile(charts.Templates, chartDir, file); err != nil {
 		return nil, err
 	}
 	values := Values{}
-	if err := yaml.Unmarshal(charts.InsertVersion(file.Data), &values); err != nil {
+	if err := yaml.Unmarshal(chartspkg.InsertVersion(file.Data), &values); err != nil {
 		return nil, err
 	}
 	return &values, nil
diff --git a/pkg/charts/charts.go b/pkg/charts/charts.go
index 7b7f16e39..9ce576c63 100644
--- a/pkg/charts/charts.go
+++ b/pkg/charts/charts.go
@@ -2,12 +2,12 @@ package charts
 
 import (
 	"bytes"
+	"embed"
 	"errors"
-	"net/http"
 	"path"
 	"strings"
 
-	"github.com/linkerd/linkerd2/pkg/charts/static"
+	"github.com/linkerd/linkerd2/charts"
 	"github.com/linkerd/linkerd2/pkg/version"
 	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/chartutil"
@@ -19,25 +19,25 @@ const versionPlaceholder = "linkerdVersionValue"
 
 var (
 	// L5dPartials is the list of templates in partials chart
-	// Keep this slice synced with the contents of /charts/partials
+	// Keep this slice synced with the contents of charts/partials
 	L5dPartials = []string{
-		"charts/partials/" + chartutil.ChartfileName,
-		"charts/partials/templates/_affinity.tpl",
-		"charts/partials/templates/_capabilities.tpl",
-		"charts/partials/templates/_debug.tpl",
-		"charts/partials/templates/_helpers.tpl",
-		"charts/partials/templates/_metadata.tpl",
-		"charts/partials/templates/_nodeselector.tpl",
-		"charts/partials/templates/_network-validator.tpl",
-		"charts/partials/templates/_proxy-config-ann.tpl",
-		"charts/partials/templates/_proxy-init.tpl",
-		"charts/partials/templates/_proxy.tpl",
-		"charts/partials/templates/_pull-secrets.tpl",
-		"charts/partials/templates/_resources.tpl",
-		"charts/partials/templates/_tolerations.tpl",
-		"charts/partials/templates/_trace.tpl",
-		"charts/partials/templates/_validate.tpl",
-		"charts/partials/templates/_volumes.tpl",
+		"partials/" + chartutil.ChartfileName,
+		"partials/templates/_affinity.tpl",
+		"partials/templates/_capabilities.tpl",
+		"partials/templates/_debug.tpl",
+		"partials/templates/_helpers.tpl",
+		"partials/templates/_metadata.tpl",
+		"partials/templates/_nodeselector.tpl",
+		"partials/templates/_network-validator.tpl",
+		"partials/templates/_proxy-config-ann.tpl",
+		"partials/templates/_proxy-init.tpl",
+		"partials/templates/_proxy.tpl",
+		"partials/templates/_pull-secrets.tpl",
+		"partials/templates/_resources.tpl",
+		"partials/templates/_tolerations.tpl",
+		"partials/templates/_trace.tpl",
+		"partials/templates/_validate.tpl",
+		"partials/templates/_volumes.tpl",
 	}
 )
 
@@ -56,7 +56,7 @@ type Chart struct {
 	Values map[string]any
 
 	Files []*loader.BufferedFile
-	Fs    http.FileSystem
+	Fs    embed.FS
 }
 
 func (c *Chart) render(partialsFiles []*loader.BufferedFile) (bytes.Buffer, error) {
@@ -64,11 +64,6 @@ func (c *Chart) render(partialsFiles []*loader.BufferedFile) (bytes.Buffer, erro
 		return bytes.Buffer{}, err
 	}
 
-	// static.Templates is used as partials are always available there
-	if err := FilesReader(static.Templates, "", partialsFiles); err != nil {
-		return bytes.Buffer{}, err
-	}
-
 	// Create chart and render templates
 	chart, err := loader.LoadFiles(append(c.Files, partialsFiles...))
 	if err != nil {
@@ -119,36 +114,40 @@ func (c *Chart) render(partialsFiles []*loader.BufferedFile) (bytes.Buffer, erro
 // Render returns a bytes buffer with the result of rendering a Helm chart
 func (c *Chart) Render() (bytes.Buffer, error) {
 
-	l5dPartials := []*loader.BufferedFile{}
-	for _, template := range L5dPartials {
-		l5dPartials = append(l5dPartials, &loader.BufferedFile{
-			Name: template,
-		})
+	partials, err := LoadPartials()
+	if err != nil {
+		return bytes.Buffer{}, err
 	}
 
-	return c.render(l5dPartials)
+	return c.render(partials)
 }
 
 // RenderCNI returns a bytes buffer with the result of rendering a Helm chart
 func (c *Chart) RenderCNI() (bytes.Buffer, error) {
 	cniPartials := []*loader.BufferedFile{
-		{Name: "charts/partials/" + chartutil.ChartfileName},
-		{Name: "charts/partials/templates/_helpers.tpl"},
-		{Name: "charts/partials/templates/_metadata.tpl"},
-		{Name: "charts/partials/templates/_pull-secrets.tpl"},
-		{Name: "charts/partials/templates/_tolerations.tpl"},
-		{Name: "charts/partials/templates/_resources.tpl"},
+		{Name: "partials/" + chartutil.ChartfileName},
+		{Name: "partials/templates/_helpers.tpl"},
+		{Name: "partials/templates/_metadata.tpl"},
+		{Name: "partials/templates/_pull-secrets.tpl"},
+		{Name: "partials/templates/_tolerations.tpl"},
+		{Name: "partials/templates/_resources.tpl"},
+	}
+
+	// Load all partial chart files into buffer
+	if err := FilesReader(charts.Templates, "", cniPartials); err != nil {
+		return bytes.Buffer{}, err
+	}
+
+	// The partials files must have the "charts/" prefix to be loaded correctly.
+	for i, f := range cniPartials {
+		cniPartials[i].Name = path.Join("charts", f.Name)
 	}
 	return c.render(cniPartials)
 }
 
 // ReadFile updates the buffered file with the data read from disk
-func ReadFile(fs http.FileSystem, dir string, f *loader.BufferedFile) error {
-	filename := dir + f.Name
-	if dir == "" {
-		filename = filename[7:]
-	}
-	file, err := fs.Open(filename)
+func ReadFile(fs embed.FS, dir string, f *loader.BufferedFile) error {
+	file, err := fs.Open(dir + f.Name)
 	if err != nil {
 		return err
 	}
@@ -164,7 +163,7 @@ func ReadFile(fs http.FileSystem, dir string, f *loader.BufferedFile) error {
 }
 
 // FilesReader reads all the files from a directory
-func FilesReader(fs http.FileSystem, dir string, files []*loader.BufferedFile) error {
+func FilesReader(fs embed.FS, dir string, files []*loader.BufferedFile) error {
 	for _, f := range files {
 		if err := ReadFile(fs, dir, f); err != nil {
 			return err
@@ -173,6 +172,26 @@ func FilesReader(fs http.FileSystem, dir string, files []*loader.BufferedFile) e
 	return nil
 }
 
+func LoadPartials() ([]*loader.BufferedFile, error) {
+	var partialFiles []*loader.BufferedFile
+	for _, template := range L5dPartials {
+		partialFiles = append(partialFiles,
+			&loader.BufferedFile{Name: template},
+		)
+	}
+
+	// Load all partial chart files into buffer
+	if err := FilesReader(charts.Templates, "", partialFiles); err != nil {
+		return nil, err
+	}
+
+	// The partials files must have the "charts/" prefix to be loaded correctly.
+	for i, f := range partialFiles {
+		partialFiles[i].Name = path.Join("charts", f.Name)
+	}
+	return partialFiles, nil
+}
+
 // InsertVersion returns the chart values file contents passed in
 // with the version placeholder replaced with the current version
 func InsertVersion(data []byte) []byte {
@@ -191,7 +210,7 @@ func InsertVersionValues(values chartutil.Values) (chartutil.Values, error) {
 }
 
 // OverrideFromFile overrides the given map with the given file from FS
-func OverrideFromFile(values map[string]interface{}, fs http.FileSystem, chartName, name string) (map[string]interface{}, error) {
+func OverrideFromFile(values map[string]interface{}, fs embed.FS, chartName, name string) (map[string]interface{}, error) {
 	// Load Values file
 	valuesOverride := loader.BufferedFile{
 		Name: name,
diff --git a/pkg/charts/cni/values.go b/pkg/charts/cni/values.go
index 935cd331c..46b56d531 100644
--- a/pkg/charts/cni/values.go
+++ b/pkg/charts/cni/values.go
@@ -3,8 +3,8 @@ package cni
 import (
 	"fmt"
 
-	"github.com/linkerd/linkerd2/pkg/charts"
-	"github.com/linkerd/linkerd2/pkg/charts/static"
+	"github.com/linkerd/linkerd2/charts"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/chartutil"
@@ -111,11 +111,11 @@ func readDefaults(chartDir string) (*Values, error) {
 	file := &loader.BufferedFile{
 		Name: chartutil.ValuesfileName,
 	}
-	if err := charts.ReadFile(static.Templates, chartDir, file); err != nil {
+	if err := chartspkg.ReadFile(charts.Templates, chartDir, file); err != nil {
 		return nil, err
 	}
 	values := Values{}
-	if err := yaml.Unmarshal(charts.InsertVersion(file.Data), &values); err != nil {
+	if err := yaml.Unmarshal(chartspkg.InsertVersion(file.Data), &values); err != nil {
 		return nil, err
 	}
 	return &values, nil
diff --git a/pkg/charts/linkerd2/values.go b/pkg/charts/linkerd2/values.go
index 6c7e3c263..48b3f4d56 100644
--- a/pkg/charts/linkerd2/values.go
+++ b/pkg/charts/linkerd2/values.go
@@ -5,8 +5,8 @@ import (
 	"fmt"
 
 	"github.com/imdario/mergo"
-	"github.com/linkerd/linkerd2/pkg/charts"
-	"github.com/linkerd/linkerd2/pkg/charts/static"
+	"github.com/linkerd/linkerd2/charts"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/version"
 	"helm.sh/helm/v3/pkg/chart/loader"
@@ -15,7 +15,7 @@ import (
 	"sigs.k8s.io/yaml"
 )
 
-const (
+var (
 	// HelmChartDirCrds is the directory name for the linkerd-crds chart
 	HelmChartDirCrds = "linkerd-crds"
 
@@ -443,12 +443,12 @@ func MergeHAValues(values *Values) error {
 // readDefaults read all the default variables from filename.
 func readDefaults(filename string) (*Values, error) {
 	valuesFile := &loader.BufferedFile{Name: filename}
-	if err := charts.ReadFile(static.Templates, "/", valuesFile); err != nil {
+	if err := chartspkg.ReadFile(charts.Templates, "", valuesFile); err != nil {
 		return nil, err
 	}
 
 	var values Values
-	err := yaml.Unmarshal(charts.InsertVersion(valuesFile.Data), &values)
+	err := yaml.Unmarshal(chartspkg.InsertVersion(valuesFile.Data), &values)
 
 	return &values, err
 }
diff --git a/pkg/charts/static/generate.go b/pkg/charts/static/generate.go
deleted file mode 100644
index ae84c0735..000000000
--- a/pkg/charts/static/generate.go
+++ /dev/null
@@ -1,21 +0,0 @@
-//go:build ignore
-
-package main
-
-import (
-	"github.com/linkerd/linkerd2/pkg/charts/static"
-	"github.com/shurcooL/vfsgen"
-	log "github.com/sirupsen/logrus"
-)
-
-func main() {
-	err := vfsgen.Generate(static.Templates, vfsgen.Options{
-		Filename:     "generated_linkerd_templates.gogen.go",
-		PackageName:  "static",
-		BuildTags:    "prod",
-		VariableName: "Templates",
-	})
-	if err != nil {
-		log.Fatalln(err)
-	}
-}
diff --git a/pkg/charts/static/helpers.go b/pkg/charts/static/helpers.go
deleted file mode 100644
index b88dea365..000000000
--- a/pkg/charts/static/helpers.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package static
-
-import (
-	"path"
-	"path/filepath"
-	"runtime"
-)
-
-// GetRepoRoot returns the full path to the root of the repo. We assume this
-// function is only called from the `Templates` var above, and that this source
-// file lives at `pkg/charts/static`, relative to the root of the repo.
-func GetRepoRoot() string {
-	// /foo/bar/linkerd2/pkg/charts/static/templates.go
-	_, filename, _, _ := runtime.Caller(0)
-
-	// /foo/bar/linkerd2/pkg/charts/static
-	dir := filepath.Dir(filename)
-
-	// filepath.Dir returns the parent directory, so that combined with joining
-	// ".." walks 3 levels up the tree:
-	// /foo/bar/linkerd2
-	return filepath.Dir(path.Join(dir, "../.."))
-}
diff --git a/pkg/charts/static/templates.go b/pkg/charts/static/templates.go
deleted file mode 100644
index e613ec36a..000000000
--- a/pkg/charts/static/templates.go
+++ /dev/null
@@ -1,13 +0,0 @@
-//go:generate go run generate.go
-//go:build !prod
-
-package static
-
-import (
-	"net/http"
-	"path"
-)
-
-// Templates that will be rendered by `linkerd install`. This is only used on
-// dev builds.
-var Templates http.FileSystem = http.Dir(path.Join(GetRepoRoot(), "charts"))
diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go
index 28e016c70..2291339f1 100644
--- a/pkg/inject/inject.go
+++ b/pkg/inject/inject.go
@@ -16,9 +16,9 @@ import (
 	"time"
 
 	jsonfilter "github.com/clarketm/json"
-	"github.com/linkerd/linkerd2/pkg/charts"
+	"github.com/linkerd/linkerd2/charts"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
-	"github.com/linkerd/linkerd2/pkg/charts/static"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/util"
 	log "github.com/sirupsen/logrus"
@@ -704,13 +704,13 @@ func GetPodPatch(conf *ResourceConfig, injectProxy bool, values *OverriddenValue
 		{Name: "templates/patch.json"},
 	}
 
-	chart := &charts.Chart{
+	chart := &chartspkg.Chart{
 		Name:      "patch",
 		Dir:       "patch",
 		Namespace: conf.namespace,
 		RawValues: rawValues,
 		Files:     files,
-		Fs:        static.Templates,
+		Fs:        charts.Templates,
 	}
 	buf, err := chart.Render()
 	if err != nil {
diff --git a/viz/charts/linkerd-viz/templates/metrics-api.yaml b/viz/charts/linkerd-viz/templates/metrics-api.yaml
index 6013da57c..036d6d550 100644
--- a/viz/charts/linkerd-viz/templates/metrics-api.yaml
+++ b/viz/charts/linkerd-viz/templates/metrics-api.yaml
@@ -74,7 +74,9 @@ spec:
       {{- include "linkerd.node-selector" (dict "Values" .Values.metricsAPI) | nindent 6 }}
       {{- $_ := set $tree "component" "metrics-api" -}}
       {{- $_ := set $tree "label" "component" -}}
-      {{- include "linkerd.affinity" $tree | nindent 6 }}
+      {{- with include "linkerd.affinity" $tree }}
+      {{- . | nindent 6 }}
+      {{- end }}
       automountServiceAccountToken: false
       containers:
       - args:
diff --git a/viz/charts/linkerd-viz/templates/tap-injector.yaml b/viz/charts/linkerd-viz/templates/tap-injector.yaml
index 7eec926bc..26b2f8e40 100644
--- a/viz/charts/linkerd-viz/templates/tap-injector.yaml
+++ b/viz/charts/linkerd-viz/templates/tap-injector.yaml
@@ -77,7 +77,9 @@ spec:
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- $_ := set $tree "component" "tap-injector" -}}
       {{- $_ := set $tree "label" "component" -}}
-      {{- include "linkerd.affinity" $tree | nindent 6 }}
+      {{- with include "linkerd.affinity" $tree }}
+      {{- . | nindent 6 }}
+      {{- end }}
       automountServiceAccountToken: false
       containers:
       - args:
diff --git a/viz/charts/linkerd-viz/templates/tap.yaml b/viz/charts/linkerd-viz/templates/tap.yaml
index 4b8b1ad8e..a997c6796 100644
--- a/viz/charts/linkerd-viz/templates/tap.yaml
+++ b/viz/charts/linkerd-viz/templates/tap.yaml
@@ -86,7 +86,9 @@ spec:
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- $_ := set $tree "component" "tap" -}}
       {{- $_ := set $tree "label" "component" -}}
-      {{- include "linkerd.affinity" $tree | nindent 6 }}
+      {{- with include "linkerd.affinity" $tree }}
+      {{- . | nindent 6 }}
+      {{- end }}
       automountServiceAccountToken: false
       containers:
       - args:
diff --git a/viz/charts/linkerd-viz/templates/web.yaml b/viz/charts/linkerd-viz/templates/web.yaml
index db8e8a938..69c86a452 100644
--- a/viz/charts/linkerd-viz/templates/web.yaml
+++ b/viz/charts/linkerd-viz/templates/web.yaml
@@ -79,7 +79,9 @@ spec:
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- $_ := set $tree "component" "web" -}}
       {{- $_ := set $tree "label" "component" -}}
-      {{- include "linkerd.affinity" $tree | nindent 6 }}
+      {{- with include "linkerd.affinity" $tree }}
+      {{- . | nindent 6 }}
+      {{- end }}
       automountServiceAccountToken: false
       containers:
       - args:
diff --git a/viz/charts/templates.go b/viz/charts/templates.go
new file mode 100644
index 000000000..7eb9af74a
--- /dev/null
+++ b/viz/charts/templates.go
@@ -0,0 +1,8 @@
+package charts
+
+import (
+	"embed"
+)
+
+//go:embed linkerd-viz
+var Templates embed.FS
diff --git a/viz/cmd/allow-scrapes.go b/viz/cmd/allow-scrapes.go
index 54a8d408e..76922b410 100644
--- a/viz/cmd/allow-scrapes.go
+++ b/viz/cmd/allow-scrapes.go
@@ -121,7 +121,7 @@ func newCmdAllowScrapes() *cobra.Command {
 	output := "yaml"
 	options := templateOptions{
 		ExtensionName: ExtensionName,
-		ChartName:     vizChartName,
+		ChartName:     VizChartName,
 		Version:       version.Version,
 		VizNs:         defaultNamespace,
 	}
diff --git a/viz/cmd/install.go b/viz/cmd/install.go
index 0e07338a3..f4dc2cd0c 100644
--- a/viz/cmd/install.go
+++ b/viz/cmd/install.go
@@ -7,12 +7,11 @@ import (
 	"path"
 	"time"
 
-	"github.com/linkerd/linkerd2/pkg/charts"
-	partials "github.com/linkerd/linkerd2/pkg/charts/static"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
-	"github.com/linkerd/linkerd2/viz/static"
+	"github.com/linkerd/linkerd2/viz/charts"
 	"github.com/spf13/cobra"
 	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/chartutil"
@@ -112,7 +111,7 @@ func install(w io.Writer, options values.Options, ha, cniEnabled bool, format st
 	}
 
 	if ha {
-		valuesOverrides, err = charts.OverrideFromFile(valuesOverrides, static.Templates, vizChartName, "values-ha.yaml")
+		valuesOverrides, err = chartspkg.OverrideFromFile(valuesOverrides, charts.Templates, VizChartName, "values-ha.yaml")
 		if err != nil {
 			return err
 		}
@@ -140,20 +139,13 @@ func render(w io.Writer, valuesOverrides map[string]interface{}, format string)
 		)
 	}
 
-	var partialFiles []*loader.BufferedFile
-	for _, template := range charts.L5dPartials {
-		partialFiles = append(partialFiles,
-			&loader.BufferedFile{Name: template},
-		)
-	}
-
 	// Load all Viz chart files into buffer
-	if err := charts.FilesReader(static.Templates, vizChartName+"/", files); err != nil {
+	if err := chartspkg.FilesReader(charts.Templates, VizChartName+"/", files); err != nil {
 		return err
 	}
 
-	// Load all partial chart files into buffer
-	if err := charts.FilesReader(partials.Templates, "", partialFiles); err != nil {
+	partialFiles, err := chartspkg.LoadPartials()
+	if err != nil {
 		return err
 	}
 
@@ -168,7 +160,7 @@ func render(w io.Writer, valuesOverrides map[string]interface{}, format string)
 		return err
 	}
 
-	vals, err = charts.InsertVersionValues(vals)
+	vals, err = chartspkg.InsertVersionValues(vals)
 	if err != nil {
 		return err
 	}
diff --git a/viz/cmd/root.go b/viz/cmd/root.go
index 5213f5edd..85b273220 100644
--- a/viz/cmd/root.go
+++ b/viz/cmd/root.go
@@ -19,7 +19,6 @@ const (
 	// until 15d1809bd043192bb21cacbc96112cce35bf384f
 	LegacyExtensionName = "linkerd-viz"
 
-	vizChartName            = "linkerd-viz"
 	defaultNamespace        = "linkerd-viz"
 	defaultLinkerdNamespace = "linkerd"
 	maxRps                  = 100.0
@@ -31,6 +30,8 @@ const (
 )
 
 var (
+	VizChartName = "linkerd-viz"
+
 	// special handling for Windows, on all other platforms these resolve to
 	// os.Stdout and os.Stderr, thanks to https://github.com/mattn/go-colorable
 	stdout = color.Output
diff --git a/viz/static/generate.go b/viz/static/generate.go
deleted file mode 100644
index 28785ffbc..000000000
--- a/viz/static/generate.go
+++ /dev/null
@@ -1,21 +0,0 @@
-//go:build ignore
-
-package main
-
-import (
-	"github.com/linkerd/linkerd2/viz/static"
-	"github.com/shurcooL/vfsgen"
-	log "github.com/sirupsen/logrus"
-)
-
-func main() {
-	err := vfsgen.Generate(static.Templates, vfsgen.Options{
-		Filename:     "generated_viz_templates.gogen.go",
-		PackageName:  "static",
-		BuildTags:    "prod",
-		VariableName: "Templates",
-	})
-	if err != nil {
-		log.Fatalln(err)
-	}
-}
diff --git a/viz/static/templates.go b/viz/static/templates.go
deleted file mode 100644
index c240343e5..000000000
--- a/viz/static/templates.go
+++ /dev/null
@@ -1,15 +0,0 @@
-//go:generate go run generate.go
-//go:build !prod
-
-package static
-
-import (
-	"net/http"
-	"path"
-
-	"github.com/linkerd/linkerd2/pkg/charts/static"
-)
-
-// Templates that will be rendered by `linkerd viz install`. This is only used on
-// dev builds.
-var Templates http.FileSystem = http.Dir(path.Join(static.GetRepoRoot(), "viz/charts"))
diff --git a/web/Dockerfile b/web/Dockerfile
index b32abbf03..f557dbf84 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -43,6 +43,7 @@ COPY viz/pkg viz/pkg
 COPY viz/tap/gen/tap viz/tap/gen/tap
 COPY viz/tap/pkg viz/tap/pkg
 COPY pkg pkg
+COPY charts charts
 ARG TARGETARCH
 RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build -mod=readonly -o web/web -ldflags "-s -w" ./web
 


Test patch:
diff --git a/cli/cmd/install_helm_test.go b/cli/cmd/install_helm_test.go
index c1ee4cdf2..b963460df 100644
--- a/cli/cmd/install_helm_test.go
+++ b/cli/cmd/install_helm_test.go
@@ -5,9 +5,9 @@ import (
 	"path/filepath"
 	"testing"
 
-	"github.com/linkerd/linkerd2/pkg/charts"
+	"github.com/linkerd/linkerd2/charts"
+	chartspkg "github.com/linkerd/linkerd2/pkg/charts"
 	l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
-	"github.com/linkerd/linkerd2/pkg/charts/static"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/testutil"
 	"helm.sh/helm/v3/pkg/chart"
@@ -227,7 +227,7 @@ func chartCrds(t *testing.T) *chart.Chart {
 
 	// Load defaults from values.yaml
 	valuesFile := &loader.BufferedFile{Name: l5dcharts.HelmChartDirCrds + "/values.yaml"}
-	if err := charts.ReadFile(static.Templates, "/", valuesFile); err != nil {
+	if err := chartspkg.ReadFile(charts.Templates, "", valuesFile); err != nil {
 		t.Fatal(err)
 	}
 	defaultValues := make(map[string]interface{})
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index c1c6740cb..be9e00490 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -951,7 +951,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1096,7 +1095,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1249,7 +1247,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1392,7 +1389,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1483,7 +1479,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1919,7 +1914,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -2010,7 +2004,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index e659ebb3f..f4c14246d 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -951,7 +951,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1095,7 +1094,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1248,7 +1246,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1391,7 +1388,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1482,7 +1478,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1917,7 +1912,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -2008,7 +2002,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index b221e15f5..8f1342bb8 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -951,7 +951,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1095,7 +1094,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1248,7 +1246,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1391,7 +1388,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1482,7 +1478,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1917,7 +1912,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -2008,7 +2002,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index e659ebb3f..f4c14246d 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -951,7 +951,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1095,7 +1094,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1248,7 +1246,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1391,7 +1388,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1482,7 +1478,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1917,7 +1912,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -2008,7 +2002,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_default_override_dst_get_nets.golden b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
index 3b1c540d4..9913fbbff 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -951,7 +951,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1095,7 +1094,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1248,7 +1246,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1391,7 +1388,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1482,7 +1478,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1917,7 +1912,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -2008,7 +2002,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_default_token.golden b/cli/cmd/testdata/install_default_token.golden
index 428c4d888..5047b7586 100644
--- a/cli/cmd/testdata/install_default_token.golden
+++ b/cli/cmd/testdata/install_default_token.golden
@@ -951,7 +951,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1095,7 +1094,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1239,7 +1237,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1382,7 +1379,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1473,7 +1469,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1899,7 +1894,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1990,7 +1984,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_gid_output.golden b/cli/cmd/testdata/install_gid_output.golden
index 5e43beb04..807b352b0 100755
--- a/cli/cmd/testdata/install_gid_output.golden
+++ b/cli/cmd/testdata/install_gid_output.golden
@@ -951,7 +951,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1096,7 +1095,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1252,7 +1250,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1395,7 +1392,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1486,7 +1482,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1928,7 +1923,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -2019,7 +2013,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 64344ec7b..61c1b1d57 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -1172,7 +1172,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1335,7 +1334,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1604,7 +1602,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -2171,7 +2168,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index 605de71c2..e6d9b3734 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -1172,7 +1172,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1335,7 +1334,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1604,7 +1602,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -2171,7 +2168,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index a8edea8d3..dac918aad 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -882,7 +882,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1026,7 +1025,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1179,7 +1177,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1322,7 +1319,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1413,7 +1409,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1764,7 +1759,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1855,7 +1849,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_helm_control_plane_output.golden b/cli/cmd/testdata/install_helm_control_plane_output.golden
index f47f7bf57..bf246a63c 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output.golden
@@ -924,7 +924,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1364,7 +1363,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1757,6 +1755,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
+      
 ---
 # Source: linkerd-control-plane/templates/heartbeat.yaml
 ---
@@ -1892,7 +1891,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-
       automountServiceAccountToken: false
       containers:
       - env:
@@ -2190,7 +2188,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-
+      
 ---
 kind: Service
 apiVersion: v1
diff --git a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
index a6be0211e..5df3d0ceb 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
@@ -1895,6 +1895,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
+      
 ---
 # Source: linkerd-control-plane/templates/heartbeat.yaml
 ---
@@ -2369,7 +2370,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-
+      
 ---
 kind: Service
 apiVersion: v1
diff --git a/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden b/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden
index bf4795a92..df96c6b6c 100755
--- a/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden
@@ -1905,6 +1905,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
+      
 ---
 # Source: linkerd-control-plane/templates/heartbeat.yaml
 ---
@@ -2384,7 +2385,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-
+      
 ---
 kind: Service
 apiVersion: v1
diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden
index 65277f484..a49ae2a59 100644
--- a/cli/cmd/testdata/install_helm_output_ha_labels.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden
@@ -1907,6 +1907,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
+      
 ---
 # Source: linkerd-control-plane/templates/heartbeat.yaml
 ---
@@ -2389,7 +2390,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-
+      
 ---
 kind: Service
 apiVersion: v1
diff --git a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
index be2b68be2..30bb5fc97 100644
--- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
@@ -1885,6 +1885,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
+      
 ---
 # Source: linkerd-control-plane/templates/heartbeat.yaml
 ---
@@ -2359,7 +2360,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-
+      
 ---
 kind: Service
 apiVersion: v1
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 9293e58bb..4f7717463 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -951,7 +951,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1095,7 +1094,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1241,7 +1239,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1384,7 +1381,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1475,7 +1471,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1903,7 +1898,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1994,7 +1988,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index 44cf2601d..c0dee9def 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -902,7 +902,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1334,7 +1333,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1731,6 +1729,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
+      
 ---
 ###
 ### Heartbeat
@@ -1860,7 +1859,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-
       automountServiceAccountToken: false
       containers:
       - env:
@@ -2156,7 +2154,7 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-
+      
 ---
 kind: Service
 apiVersion: v1
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index 94be2abcf..41e21ff04 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -951,7 +951,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1095,7 +1094,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1248,7 +1246,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1391,7 +1388,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1482,7 +1478,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1917,7 +1912,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -2008,7 +2002,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index 5c1035b9a..6d01b761f 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -951,7 +951,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1095,7 +1094,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1248,7 +1246,6 @@ spec:
       - emptyDir:
           medium: Memory
         name: linkerd-identity-end-entity
-      
 ---
 ###
 ### Destination Controller Service
@@ -1391,7 +1388,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -1482,7 +1478,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
@@ -1917,7 +1912,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - env:
@@ -2008,7 +2002,6 @@ spec:
           value: 30s
         - name: LINKERD2_PROXY_OUTBOUND_METRICS_HOSTNAME_LABELS
           value: "false"
-        
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_INTERVAL
           value: "10s"
         - name: LINKERD2_PROXY_INBOUND_SERVER_HTTP2_KEEP_ALIVE_TIMEOUT
diff --git a/multicluster/cmd/testdata/install_default.golden b/multicluster/cmd/testdata/install_default.golden
index 1d445b705..4233505df 100644
--- a/multicluster/cmd/testdata/install_default.golden
+++ b/multicluster/cmd/testdata/install_default.golden
@@ -39,7 +39,6 @@ spec:
         app: linkerd-gateway
         linkerd.io/extension: multicluster
     spec:
-      
       automountServiceAccountToken: false
       containers:
       - name: pause
diff --git a/multicluster/cmd/testdata/install_psp.golden b/multicluster/cmd/testdata/install_psp.golden
index c3257f875..f69d7a7be 100644
--- a/multicluster/cmd/testdata/install_psp.golden
+++ b/multicluster/cmd/testdata/install_psp.golden
@@ -39,7 +39,6 @@ spec:
         app: linkerd-gateway
         linkerd.io/extension: multicluster
     spec:
-      
       automountServiceAccountToken: false
       containers:
       - name: pause
diff --git a/test/integration/viz/edges/testdata/linkerd_edges.golden b/test/integration/viz/edges/testdata/linkerd_edges.golden
index 5753225a5..09747eda9 100644
--- a/test/integration/viz/edges/testdata/linkerd_edges.golden
+++ b/test/integration/viz/edges/testdata/linkerd_edges.golden
@@ -26,4 +26,4 @@
     "server_id": "linkerd\-proxy\-injector.{{.Ns}}",
     "no_tls_reason": ""
   \}
-\]
\ No newline at end of file
+\]
diff --git a/viz/cmd/testdata/install_default.golden b/viz/cmd/testdata/install_default.golden
index 8beb6f50a..220e4cca7 100644
--- a/viz/cmd/testdata/install_default.golden
+++ b/viz/cmd/testdata/install_default.golden
@@ -419,7 +419,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -934,7 +933,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1174,7 +1172,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1368,7 +1365,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
diff --git a/viz/cmd/testdata/install_default_overrides.golden b/viz/cmd/testdata/install_default_overrides.golden
index 69111ccf8..e5a8951cf 100644
--- a/viz/cmd/testdata/install_default_overrides.golden
+++ b/viz/cmd/testdata/install_default_overrides.golden
@@ -419,7 +419,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -934,7 +933,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1174,7 +1172,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1368,7 +1365,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
diff --git a/viz/cmd/testdata/install_prometheus_disabled.golden b/viz/cmd/testdata/install_prometheus_disabled.golden
index b659b5b95..a4c4e381e 100644
--- a/viz/cmd/testdata/install_prometheus_disabled.golden
+++ b/viz/cmd/testdata/install_prometheus_disabled.golden
@@ -379,7 +379,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -620,7 +619,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -860,7 +858,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1054,7 +1051,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
diff --git a/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden b/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
index 141fde2ee..f0296a091 100644
--- a/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
+++ b/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
@@ -419,7 +419,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -934,7 +933,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1174,7 +1172,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1368,7 +1365,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
diff --git a/viz/cmd/testdata/install_proxy_resources.golden b/viz/cmd/testdata/install_proxy_resources.golden
index ad33e05ec..61f83b368 100644
--- a/viz/cmd/testdata/install_proxy_resources.golden
+++ b/viz/cmd/testdata/install_proxy_resources.golden
@@ -419,7 +419,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -942,7 +941,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1182,7 +1180,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args:
@@ -1380,7 +1377,6 @@ spec:
     spec:
       nodeSelector:
         kubernetes.io/os: linux
-      
       automountServiceAccountToken: false
       containers:
       - args: