Repository: linkerd/linkerd2
Pull request #6782 (2021-08-30 20:02:19)

Introduce LINKERD_DOCKER_REGISTRY and flexibilize CI workflows
This allows overriding the docker images registry through the
`LINKERD_DOCKER_REGISTRY` environment variable, as the CLI's `--registry`
flag does (analogously to the `LINKERD_NAMESPACE` env var).

In the `integration_tests.yml` workflow we've been
wanting to test directly against GH's registry instead of having to
endure cr.l5d.io's problems. To achieve this we set `DOCKER_REGISTRY:
ghcr.io/linkerd` globally,
and all the docker scripts will use it
accordingly. And before triggering the integration tests, we copy that
value to `LINKERD_DOCKER_REGISTRY`. Note that the CLI and Helm manifests
will continue to point to `cr.l5d.io` by default when this is not
overridden.

`release.yml` still points to `cr.l5.io` because that's what users will experience,
so we want to test that right before releasing.

Another goal was to be able to run these workflows in forks by
overriding the global `DOCKER_REGISTRY` var pointing to the fork's own
registry.

## Nice side-effects

- This fixes the policy controller's image not being affected by the
`--registry` flag when installing through the CLI.
- The `--registry` flag (and overriding `LINKERD_DOCKER_REGISTRY` env var) have been added to `linkerd jaeger install`.


alpeb: I've changed my mind regarding `release.yml` (and updated the description above accordingly). I think that one should still point to `cr.l5d.io/linkerd` given that's what users will experience, so we'd like to test that right before releasing (e.g. to make sure any new image has been configured in `cr.l5d.io/linkerd` correctly). Note this could be be easily set back to `ghcr.io/linkerd` if we still want both workflows to behave similarly.
alpeb: Test failures revealed that the multicluster and jaeger extension also required equivalent changes. I've just pushed that, and updated this PR's description.

Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cb5ec6e9b..089de9664 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,6 +7,7 @@ permissions:
   contents: read
 env:
   GH_ANNOTATION: true
+  DOCKER_REGISTRY: cr.l5d.io/linkerd
 jobs:
   docker_build:
     runs-on: ubuntu-20.04
@@ -23,9 +24,6 @@ jobs:
       run: |
         . bin/_tag.sh
         echo "TAG=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> $GITHUB_ENV
-
-        . bin/_docker.sh
-        echo "DOCKER_REGISTRY=ghcr.io/linkerd" >> $GITHUB_ENV
         echo "DOCKER_BUILDKIT_CACHE=${{ runner.temp }}/.buildx-cache" >> $GITHUB_ENV
     - name: Cache docker layers
       uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353
@@ -90,7 +88,7 @@ jobs:
         echo "${{ secrets.DOCKER_GHCR_PAT }}" | docker login ghcr.io -u "${{ secrets.DOCKER_GHCR_USERNAME }}" --password-stdin
         docker buildx build --push ./policy-controller/ \
           -f ./policy-controller/${{ matrix.arch }}.dockerfile \
-          -t ghcr.io/linkerd/policy-controller:$TAG-${{ matrix.arch }}
+          -t $DOCKER_REGISTRY/policy-controller:$TAG-${{ matrix.arch }}
 
   policy_controller_manifest:
     runs-on: ubuntu-20.04
@@ -105,22 +103,22 @@ jobs:
         echo "TAG=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> $GITHUB_ENV
     - name: Create multiarch manifest
       run: |
-        docker manifest create ghcr.io/linkerd/policy-controller:${TAG} \
-            ghcr.io/linkerd/policy-controller:${TAG}-amd64 \
-            ghcr.io/linkerd/policy-controller:${TAG}-arm64 \
-            ghcr.io/linkerd/policy-controller:${TAG}-arm
+        docker manifest create $DOCKER_REGISTRY/policy-controller:$TAG \
+            $DOCKER_REGISTRY/policy-controller:$TAG-amd64 \
+            $DOCKER_REGISTRY/policy-controller:$TAG-arm64 \
+            $DOCKER_REGISTRY/policy-controller:$TAG-arm
     - name: Annotate multiarch manifest
       run: |
-        docker manifest annotate ghcr.io/linkerd/policy-controller:$TAG \
-            ghcr.io/linkerd/policy-controller:${TAG}-amd64 --os=linux --arch=amd64
-        docker manifest annotate ghcr.io/linkerd/policy-controller:$TAG \
-            ghcr.io/linkerd/policy-controller:${TAG}-arm64 --os=linux --arch=arm64
-        docker manifest annotate ghcr.io/linkerd/policy-controller:$TAG \
-            ghcr.io/linkerd/policy-controller:${TAG}-arm --os=linux --arch=arm
+        docker manifest annotate $DOCKER_REGISTRY/policy-controller:$TAG \
+            $DOCKER_REGISTRY/policy-controller:${TAG}-amd64 --os=linux --arch=amd64
+        docker manifest annotate $DOCKER_REGISTRY/policy-controller:$TAG \
+            $DOCKER_REGISTRY/policy-controller:${TAG}-arm64 --os=linux --arch=arm64
+        docker manifest annotate $DOCKER_REGISTRY/policy-controller:$TAG \
+            $DOCKER_REGISTRY/policy-controller:${TAG}-arm --os=linux --arch=arm
     - name: Push multiarch manifest
       run: |
         echo "${{ secrets.DOCKER_GHCR_PAT }}" | docker login ghcr.io -u "${{ secrets.DOCKER_GHCR_USERNAME }}" --password-stdin
-        docker manifest push ghcr.io/linkerd/policy-controller:$TAG
+        docker manifest push $DOCKER_REGISTRY/policy-controller:$TAG
 
   # todo: Keep in sync with `integration_tests.yml`
   windows_static_cli_tests:
@@ -180,6 +178,8 @@ jobs:
         echo "CMD=$CMD" >> $GITHUB_ENV
         echo "TAG=$TAG" >> $GITHUB_ENV
     - name: Run integration tests
+      env:
+        LINKERD_DOCKER_REGISTRY: ${{ env.DOCKER_REGISTRY }}
       run: |
         bin/docker-pull-binaries $TAG
         # Validate the CLI version matches the current build tag.
@@ -220,6 +220,7 @@ jobs:
     - name: Run integration tests
       env:
         RUN_ARM_TEST: 1
+        LINKERD_DOCKER_REGISTRY: ${{ env.DOCKER_REGISTRY }}
       run: bin/tests --name deep --images preload --skip-cluster-create "$CMD"
     - name: CNI tests
       run: |
diff --git a/cli/cmd/install-cni-plugin.go b/cli/cmd/install-cni-plugin.go
index 51d3811c2..16980e854 100644
--- a/cli/cmd/install-cni-plugin.go
+++ b/cli/cmd/install-cni-plugin.go
@@ -9,6 +9,8 @@ import (
 	"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"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -66,8 +68,12 @@ func (options *cniPluginOptions) validate() error {
 }
 
 func (options *cniPluginOptions) pluginImage() string {
+	// env var overrides CLI flag
+	if override := os.Getenv(flags.EnvOverrideDockerRegistry); override != "" {
+		return cmd.RegistryOverride(options.cniPluginImage, override)
+	}
 	if options.dockerRegistry != defaultDockerRegistry {
-		return registryOverride(options.cniPluginImage, options.dockerRegistry)
+		return cmd.RegistryOverride(options.cniPluginImage, options.dockerRegistry)
 	}
 	return options.cniPluginImage
 }
@@ -95,7 +101,8 @@ assumes that the 'linkerd install' command will be executed with the
 	}
 
 	cmd.PersistentFlags().StringVarP(&options.linkerdVersion, "linkerd-version", "v", options.linkerdVersion, "Tag to be used for Linkerd images")
-	cmd.PersistentFlags().StringVar(&options.dockerRegistry, "registry", options.dockerRegistry, "Docker registry to pull images from")
+	cmd.PersistentFlags().StringVar(&options.dockerRegistry, "registry", options.dockerRegistry,
+		fmt.Sprintf("Docker registry to pull images from ($%s)", flags.EnvOverrideDockerRegistry))
 	cmd.PersistentFlags().Int64Var(&options.proxyUID, "proxy-uid", options.proxyUID, "Run the proxy under this user ID")
 	cmd.PersistentFlags().UintVar(&options.inboundPort, "inbound-port", options.inboundPort, "Proxy port to use for inbound traffic")
 	cmd.PersistentFlags().UintVar(&options.outboundPort, "outbound-port", options.outboundPort, "Proxy port to use for outbound traffic")
diff --git a/cli/cmd/options.go b/cli/cmd/options.go
index 6c2b0d600..24c383e1a 100644
--- a/cli/cmd/options.go
+++ b/cli/cmd/options.go
@@ -3,6 +3,7 @@ package cmd
 import (
 	"context"
 	"errors"
+	"os"
 
 	"fmt"
 	"io/ioutil"
@@ -12,6 +13,8 @@ import (
 
 	"github.com/linkerd/linkerd2/cli/flag"
 	l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
+	"github.com/linkerd/linkerd2/pkg/cmd"
+	flagspkg "github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/inject"
 	"github.com/linkerd/linkerd2/pkg/issuercerts"
 	"github.com/linkerd/linkerd2/pkg/k8s"
@@ -305,15 +308,6 @@ func makeProxyFlags(defaults *l5dcharts.Values) ([]flag.Flag, *pflag.FlagSet) {
 				return nil
 			}),
 
-		flag.NewStringFlag(proxyFlags, "registry", defaultDockerRegistry, "Docker registry to pull images from",
-			func(values *l5dcharts.Values, value string) error {
-				values.ControllerImage = registryOverride(values.ControllerImage, value)
-				values.DebugContainer.Image.Name = registryOverride(values.DebugContainer.Image.Name, value)
-				values.Proxy.Image.Name = registryOverride(values.Proxy.Image.Name, value)
-				values.ProxyInit.Image.Name = registryOverride(values.ProxyInit.Image.Name, value)
-				return nil
-			}),
-
 		flag.NewStringFlag(proxyFlags, "image-pull-policy", defaults.ImagePullPolicy,
 			"Docker image pull policy", func(values *l5dcharts.Values, value string) error {
 				values.ImagePullPolicy = value
@@ -425,6 +419,21 @@ func makeProxyFlags(defaults *l5dcharts.Values) ([]flag.Flag, *pflag.FlagSet) {
 			}),
 	}
 
+	registryFlag := flag.NewStringFlag(proxyFlags, "registry", defaultDockerRegistry,
+		fmt.Sprintf("Docker registry to pull images from ($%s)", flagspkg.EnvOverrideDockerRegistry),
+		func(values *l5dcharts.Values, value string) error {
+			values.ControllerImage = cmd.RegistryOverride(values.ControllerImage, value)
+			values.PolicyController.Image.Name = cmd.RegistryOverride(values.PolicyController.Image.Name, value)
+			values.DebugContainer.Image.Name = cmd.RegistryOverride(values.DebugContainer.Image.Name, value)
+			values.Proxy.Image.Name = cmd.RegistryOverride(values.Proxy.Image.Name, value)
+			values.ProxyInit.Image.Name = cmd.RegistryOverride(values.ProxyInit.Image.Name, value)
+			return nil
+		})
+	if reg := os.Getenv(flagspkg.EnvOverrideDockerRegistry); reg != "" {
+		registryFlag.Set(reg)
+	}
+	flags = append(flags, registryFlag)
+
 	proxyFlags.MarkDeprecated("proxy-memory", "use --proxy-memory-request instead")
 	proxyFlags.MarkDeprecated("proxy-cpu", "use --proxy-cpu-request instead")
 
diff --git a/cli/cmd/root.go b/cli/cmd/root.go
index 05f834170..27a8f704e 100644
--- a/cli/cmd/root.go
+++ b/cli/cmd/root.go
@@ -11,6 +11,7 @@ import (
 	jaeger "github.com/linkerd/linkerd2/jaeger/cmd"
 	multicluster "github.com/linkerd/linkerd2/multicluster/cmd"
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
+	"github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	viz "github.com/linkerd/linkerd2/viz/cmd"
 	log "github.com/sirupsen/logrus"
@@ -79,7 +80,7 @@ var RootCmd = &cobra.Command{
 			log.SetLevel(log.PanicLevel)
 		}
 
-		controlPlaneNamespaceFromEnv := os.Getenv("LINKERD_NAMESPACE")
+		controlPlaneNamespaceFromEnv := os.Getenv(flags.EnvOverrideNamespace)
 		if controlPlaneNamespace == defaultLinkerdNamespace && controlPlaneNamespaceFromEnv != "" {
 			controlPlaneNamespace = controlPlaneNamespaceFromEnv
 		}
@@ -93,7 +94,9 @@ var RootCmd = &cobra.Command{
 }
 
 func init() {
-	RootCmd.PersistentFlags().StringVarP(&controlPlaneNamespace, "linkerd-namespace", "L", defaultLinkerdNamespace, "Namespace in which Linkerd is installed ($LINKERD_NAMESPACE)")
+	RootCmd.PersistentFlags().StringVarP(&controlPlaneNamespace, "linkerd-namespace", "L",
+		defaultLinkerdNamespace,
+		fmt.Sprintf("Namespace in which Linkerd is installed ($%s)", flags.EnvOverrideNamespace))
 	RootCmd.PersistentFlags().StringVarP(&cniNamespace, "cni-namespace", "", defaultCNINamespace, "Namespace in which the Linkerd CNI plugin is installed")
 	RootCmd.PersistentFlags().StringVar(&kubeconfigPath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests")
 	RootCmd.PersistentFlags().StringVar(&kubeContext, "context", "", "Name of the kubeconfig context to use")
@@ -143,22 +146,6 @@ func deprecateCmd(cmd *cobra.Command) *cobra.Command {
 	return cmd
 }
 
-// registryOverride replaces the registry-portion of the provided image with the provided registry.
-func registryOverride(image, newRegistry string) string {
-	if image == "" {
-		return image
-	}
-	registry := newRegistry
-	if registry != "" && !strings.HasSuffix(registry, slash) {
-		registry += slash
-	}
-	imageName := image
-	if strings.Contains(image, slash) {
-		imageName = image[strings.LastIndex(image, slash)+1:]
-	}
-	return registry + imageName
-}
-
 func flattenFlags(flags ...[]flag.Flag) []flag.Flag {
 	out := []flag.Flag{}
 	for _, f := range flags {
diff --git a/cli/flag/flag.go b/cli/flag/flag.go
index 46311df55..c44bb1fe4 100644
--- a/cli/flag/flag.go
+++ b/cli/flag/flag.go
@@ -186,6 +186,11 @@ func (flag *StringFlag) Apply(values *charts.Values) error {
 	return flag.apply(values, flag.Value)
 }
 
+// Set sets the given value to the underlying Flag
+func (flag *StringFlag) Set(value string) error {
+	return flag.flagSet.Set(flag.name, value)
+}
+
 // IsSet returns true if and only if the Flag has been explicitly set with a value.
 func (flag *StringFlag) IsSet() bool {
 	return flag.flagSet.Changed(flag.name)
diff --git a/jaeger/cmd/install.go b/jaeger/cmd/install.go
index 231d4ddb2..6fd228fe5 100644
--- a/jaeger/cmd/install.go
+++ b/jaeger/cmd/install.go
@@ -2,6 +2,7 @@ package cmd
 
 import (
 	"bytes"
+	"fmt"
 	"io"
 	"os"
 	"path"
@@ -10,6 +11,7 @@ import (
 	"github.com/linkerd/linkerd2/jaeger/static"
 	"github.com/linkerd/linkerd2/pkg/charts"
 	partials "github.com/linkerd/linkerd2/pkg/charts/static"
+	"github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	api "github.com/linkerd/linkerd2/pkg/public"
@@ -31,6 +33,7 @@ var (
 )
 
 func newCmdInstall() *cobra.Command {
+	var registry string
 	var skipChecks bool
 	var wait time.Duration
 	var options values.Options
@@ -62,10 +65,12 @@ A full list of configurable values can be found at https://www.github.com/linker
 				})
 			}
 
-			return install(os.Stdout, options)
+			return install(os.Stdout, options, registry)
 		},
 	}
 
+	cmd.Flags().StringVar(&registry, "registry", "cr.l5d.io/linkerd",
+		fmt.Sprintf("Docker registry to pull jaeger-webhook image from ($%s)", flags.EnvOverrideDockerRegistry))
 	cmd.Flags().BoolVar(&skipChecks, "skip-checks", false, `Skip checks for linkerd core control-plane existence`)
 	cmd.Flags().DurationVar(&wait, "wait", 300*time.Second, "Wait for core control-plane components to be available")
 
@@ -74,7 +79,7 @@ A full list of configurable values can be found at https://www.github.com/linker
 	return cmd
 }
 
-func install(w io.Writer, options values.Options) error {
+func install(w io.Writer, options values.Options, registry string) error {
 
 	// Create values override
 	valuesOverrides, err := options.MergeValues(nil)
@@ -84,10 +89,10 @@ func install(w io.Writer, options values.Options) error {
 
 	// TODO: Add any validation logic here
 
-	return render(w, valuesOverrides)
+	return render(w, valuesOverrides, registry)
 }
 
-func render(w io.Writer, valuesOverrides map[string]interface{}) error {
+func render(w io.Writer, valuesOverrides map[string]interface{}, registry string) error {
 
 	files := []*loader.BufferedFile{
 		{Name: chartutil.ChartfileName},
@@ -133,6 +138,15 @@ func render(w io.Writer, valuesOverrides map[string]interface{}) error {
 		return err
 	}
 
+	regOrig := vals["webhook"].(map[string]interface{})["image"].(map[string]interface{})["name"].(string)
+	if registry != "" {
+		vals["webhook"].(map[string]interface{})["image"].(map[string]interface{})["name"] = cmd.RegistryOverride(regOrig, registry)
+	}
+	// env var overrides CLI flag
+	if override := os.Getenv(flags.EnvOverrideDockerRegistry); override != "" {
+		vals["webhook"].(map[string]interface{})["image"].(map[string]interface{})["name"] = cmd.RegistryOverride(regOrig, override)
+	}
+
 	// Attach the final values into the `Values` field for rendering to work
 	renderedTemplates, err := engine.Render(chart, map[string]interface{}{"Values": vals})
 	if err != nil {
diff --git a/multicluster/cmd/link.go b/multicluster/cmd/link.go
index 55edae876..6c472e39e 100644
--- a/multicluster/cmd/link.go
+++ b/multicluster/cmd/link.go
@@ -327,7 +327,8 @@ A full list of configurable values can be found at https://github.com/linkerd/li
 	cmd.Flags().StringVar(&opts.gatewayNamespace, "gateway-namespace", defaultMulticlusterNamespace, "The namespace of the gateway service")
 	cmd.Flags().Uint32Var(&opts.serviceMirrorRetryLimit, "service-mirror-retry-limit", opts.serviceMirrorRetryLimit, "The number of times a failed update from the target cluster is allowed to be retried")
 	cmd.Flags().StringVar(&opts.logLevel, "log-level", opts.logLevel, "Log level for the Multicluster components")
-	cmd.Flags().StringVar(&opts.dockerRegistry, "registry", opts.dockerRegistry, "Docker registry to pull service mirror controller image from")
+	cmd.Flags().StringVar(&opts.dockerRegistry, "registry", opts.dockerRegistry,
+		fmt.Sprintf("Docker registry to pull service mirror controller image from ($%s)", flags.EnvOverrideDockerRegistry))
 	cmd.Flags().StringVarP(&opts.selector, "selector", "l", opts.selector, "Selector (label query) to filter which services in the target cluster to mirror")
 	cmd.Flags().StringVar(&opts.gatewayAddresses, "gateway-addresses", opts.gatewayAddresses, "If specified, overwrites gateway addresses when gateway service is not type LoadBalancer (comma separated list)")
 	cmd.Flags().Uint32Var(&opts.gatewayPort, "gateway-port", opts.gatewayPort, "If specified, overwrites gateway port when gateway service is not type LoadBalancer")
@@ -379,6 +380,10 @@ func buildServiceMirrorValues(opts *linkOptions) (*multicluster.Values, error) {
 		return nil, err
 	}
 
+	if reg := os.Getenv(flags.EnvOverrideDockerRegistry); reg != "" {
+		opts.dockerRegistry = reg
+	}
+
 	defaults.TargetClusterName = opts.clusterName
 	defaults.Namespace = opts.namespace
 	defaults.ServiceMirrorRetryLimit = opts.serviceMirrorRetryLimit
diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go
index 8f1e32d1c..2b0808298 100644
--- a/pkg/cmd/cmd.go
+++ b/pkg/cmd/cmd.go
@@ -146,3 +146,19 @@ func GetLabelSelector(labelKey string, labelValues ...string) (string, error) {
 	selector := labels.NewSelector().Add(*labelRequirement)
 	return selector.String(), nil
 }
+
+// RegistryOverride replaces the registry-portion of the provided image with the provided registry.
+func RegistryOverride(image, newRegistry string) string {
+	if image == "" {
+		return image
+	}
+	registry := newRegistry
+	if registry != "" && !strings.HasSuffix(registry, "/") {
+		registry += "/"
+	}
+	imageName := image
+	if strings.Contains(image, "/") {
+		imageName = image[strings.LastIndex(image, "/")+1:]
+	}
+	return registry + imageName
+}
diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go
index 82ca13b0b..71f66d736 100644
--- a/pkg/flags/flags.go
+++ b/pkg/flags/flags.go
@@ -12,6 +12,18 @@ import (
 	klog "k8s.io/klog/v2"
 )
 
+const (
+
+	// EnvOverrideNamespace is the environment variable used in the CLI to
+	// overidde the control-plane's namespace
+	EnvOverrideNamespace = "LINKERD_NAMESPACE"
+
+	// EnvOverrideDockerRegistry is the environment variable used in the
+	// CLI to override the docker images' registry in the control-plane
+	// manifests
+	EnvOverrideDockerRegistry = "LINKERD_DOCKER_REGISTRY"
+)
+
 // ConfigureAndParse adds flags that are common to all go processes. This
 // func calls flag.Parse(), so it should be called after all other flags have
 // been configured.
diff --git a/viz/cmd/install.go b/viz/cmd/install.go
index 82d78f70a..ea67f9f0f 100644
--- a/viz/cmd/install.go
+++ b/viz/cmd/install.go
@@ -92,11 +92,13 @@ func install(w io.Writer, options values.Options, ha bool) error {
 		return err
 	}
 
-	// if using -L to specify a non-standard CP namespace, make sure
-	// the linkerdNamespace Helm value is synced
+	// sync values overrides with Helm values
 	if controlPlaneNamespace != defaultLinkerdNamespace {
 		valuesOverrides["linkerdNamespace"] = controlPlaneNamespace
 	}
+	if reg := os.Getenv(flags.EnvOverrideDockerRegistry); reg != "" {
+		valuesOverrides["defaultRegistry"] = reg
+	}
 
 	if ha {
 		valuesOverrides, err = charts.OverrideFromFile(valuesOverrides, static.Templates, vizChartName, "values-ha.yaml")


Test patch:
diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml
index e173604eb..231a53bae 100644
--- a/.github/workflows/integration_tests.yml
+++ b/.github/workflows/integration_tests.yml
@@ -12,6 +12,7 @@ permissions:
   contents: read
 env:
   GH_ANNOTATION: true
+  DOCKER_REGISTRY: ghcr.io/linkerd
 jobs:
   docker_build:
     runs-on: ubuntu-20.04
@@ -28,9 +29,6 @@ jobs:
       run: |
         . bin/_tag.sh
         echo "TAG=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> $GITHUB_ENV
-
-        . bin/_docker.sh
-        echo "DOCKER_REGISTRY=cr.l5d.io/linkerd" >> $GITHUB_ENV
         echo "DOCKER_BUILDKIT_CACHE=${{ runner.temp }}/.buildx-cache" >> $GITHUB_ENV
     - name: Cache docker layers
       uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353
@@ -54,7 +52,7 @@ jobs:
         ARCHIVES: /home/runner/archives
       run: |
         mkdir -p $ARCHIVES
-        docker save "cr.l5d.io/linkerd/${{ matrix.target }}:$TAG" > $ARCHIVES/${{ matrix.target }}.tar
+        docker save "$DOCKER_REGISTRY/${{ matrix.target }}:$TAG" > $ARCHIVES/${{ matrix.target }}.tar
     # `with.path` values do not support environment variables yet, so an
     # absolute path is used here.
     #
@@ -99,9 +97,6 @@ jobs:
       run: |
         . bin/_tag.sh
         echo "TAG=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> $GITHUB_ENV
-
-        . bin/_docker.sh
-        echo "DOCKER_REGISTRY=cr.l5d.io/linkerd" >> $GITHUB_ENV
     - name: Download image archives
       uses: actions/download-artifact@3be87be14a055c47b01d3bd88f8fe02320a9bb60
       with:
@@ -112,11 +107,13 @@ jobs:
     - name: Install CLI
       run: |
         # Copy the CLI out of the local cli-bin container.
-        container_id=$(docker create "cr.l5d.io/linkerd/cli-bin:$TAG")
+        container_id=$(docker create "$DOCKER_REGISTRY/cli-bin:$TAG")
         docker cp $container_id:/out/linkerd-linux-amd64 "$HOME/.linkerd"
 
         # Validate the CLI version matches the current build tag.
         [[ "$TAG" == "$($HOME/.linkerd version --short --client)" ]]
     - name: Run integration tests
+      env:
+        LINKERD_DOCKER_REGISTRY: ${{ env.DOCKER_REGISTRY }}
       run: |
         bin/tests --images archive --cleanup-docker --name ${{ matrix.integration_test }} "$HOME/.linkerd"
diff --git a/cli/cmd/inject_test.go b/cli/cmd/inject_test.go
index 12c1ca104..772ec5af7 100644
--- a/cli/cmd/inject_test.go
+++ b/cli/cmd/inject_test.go
@@ -12,6 +12,7 @@ import (
 	"testing"
 
 	"github.com/linkerd/linkerd2/pkg/charts/linkerd2"
+	"github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/testutil"
 )
@@ -760,7 +761,7 @@ func TestOverwriteRegistry(t *testing.T) {
 	for i, tc := range testCases {
 		tc := tc // pin
 		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
-			actual := registryOverride(tc.image, tc.registry)
+			actual := cmd.RegistryOverride(tc.image, tc.registry)
 			if actual != tc.expected {
 				t.Fatalf("expected %q, but got %q", tc.expected, actual)
 			}
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index 5bf8c2ee8..f2eaa398e 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -1099,7 +1099,7 @@ data:
     policyController:
       defaultAllowPolicy: all-unauthenticated
       image:
-        name: cr.l5d.io/linkerd/policy-controller
+        name: my.custom.registry/linkerd-io/policy-controller
         pullPolicy: ""
         version: install-control-plane-version
       logLevel: linkerd=info,warn
@@ -1832,7 +1832,7 @@ spec:
         env:
         - name: RUST_LOG
           value: "linkerd=info,warn"
-        image: cr.l5d.io/linkerd/policy-controller:install-control-plane-version
+        image: my.custom.registry/linkerd-io/policy-controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
@@ -2221,7 +2221,7 @@ spec:
 ---
 apiVersion: v1
 data:
-  linkerd-config-overrides: Y29udHJvbGxlckltYWdlOiBteS5jdXN0b20ucmVnaXN0cnkvbGlua2VyZC1pby9jb250cm9sbGVyCmNvbnRyb2xsZXJJbWFnZVZlcnNpb246IGluc3RhbGwtY29udHJvbC1wbGFuZS12ZXJzaW9uCmRlYnVnQ29udGFpbmVyOgogIGltYWdlOgogICAgbmFtZTogbXkuY3VzdG9tLnJlZ2lzdHJ5L2xpbmtlcmQtaW8vZGVidWcKICAgIHZlcnNpb246IGluc3RhbGwtZGVidWctdmVyc2lvbgpoZWFydGJlYXRTY2hlZHVsZTogMSAyIDMgNCA1CmlkZW50aXR5OgogIGlzc3VlcjoKICAgIGNydEV4cGlyeTogIjIwMzAtMDgtMjZUMDc6MTM6NDdaIgogICAgdGxzOgogICAgICBjcnRQRU06IHwKICAgICAgICAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KICAgICAgICBNSUlCd0RDQ0FXZWdBd0lCQWdJUkFKUklnWjhSdE84RXdnMVhlcGY4VDQ0d0NnWUlLb1pJemowRUF3SXdLVEVuCiAgICAgICAgTUNVR0ExVUVBeE1lYVdSbGJuUnBkSGt1YkdsdWEyVnlaQzVqYkhWemRHVnlMbXh2WTJGc01CNFhEVEl3TURneQogICAgICAgIE9EQTNNVE0wTjFvWERUTXdNRGd5TmpBM01UTTBOMW93S1RFbk1DVUdBMVVFQXhNZWFXUmxiblJwZEhrdWJHbHUKICAgICAgICBhMlZ5WkM1amJIVnpkR1Z5TG14dlkyRnNNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUxL0ZwCiAgICAgICAgZmNSbkRjZWRMNkFqVWFYWVB2NERJTUJhSnVmT0k1Tld0eStYU1g3SmpYZ1p0TTcyZFF2UmFZYW51eEQzNkR0MQogICAgICAgIDIvSnh5aVNneEtXUmRvYXkrYU53TUc0d0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUIKICAgICAgICBBZjhDQVFBd0hRWURWUjBPQkJZRUZJMVducnFNWUthSEhPbyt6cHlpaURxMnBPMEtNQ2tHQTFVZEVRUWlNQ0NDCiAgICAgICAgSG1sa1pXNTBhWFI1TG14cGJtdGxjbVF1WTJ4MWMzUmxjaTVzYjJOaGJEQUtCZ2dxaGtqT1BRUURBZ05IQURCRQogICAgICAgIEFpQXR1b0k1WHVDdHJHVlJ6U21SVGwycmEyOGFWOU15VFU3ZDVxblRBRkhLU2dJZ1JLQ3ZsdU9TZ0E1TzIxcDUKICAgICAgICA1MXRkcm1rSEVaUnIwcWxMU0pkSFlnRWZNems9CiAgICAgICAgLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQogICAgICBrZXlQRU06IHwKICAgICAgICAtLS0tLUJFR0lOIEVDIFBSSVZBVEUgS0VZLS0tLS0KICAgICAgICBNSGNDQVFFRUlBQWU4bmZielp1OWMvT0IyKzh4Sk0wRno3TlV3VFFhenVsa0ZOczRUSTUrb0FvR0NDcUdTTTQ5CiAgICAgICAgQXdFSG9VUURRZ0FFMS9GcGZjUm5EY2VkTDZBalVhWFlQdjRESU1CYUp1Zk9JNU5XdHkrWFNYN0pqWGdadE03MgogICAgICAgIGRRdlJhWWFudXhEMzZEdDEyL0p4eWlTZ3hLV1Jkb2F5K1E9PQogICAgICAgIC0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0KaWRlbnRpdHlUcnVzdEFuY2hvcnNQRU06IHwKICAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KICBNSUlCd1RDQ0FXYWdBd0lCQWdJUWVEWnA1bERhSXlnUTVVZk1LWnJGQVRBS0JnZ3Foa2pPUFFRREFqQXBNU2N3CiAgSlFZRFZRUURFeDVwWkdWdWRHbDBlUzVzYVc1clpYSmtMbU5zZFhOMFpYSXViRzlqWVd3d0hoY05NakF3T0RJNAogIE1EY3hNalEzV2hjTk16QXdPREkyTURjeE1qUTNXakFwTVNjd0pRWURWUVFERXg1cFpHVnVkR2wwZVM1c2FXNXIKICBaWEprTG1Oc2RYTjBaWEl1Ykc5allXd3dXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUnFjNzBaCiAgbDF2Z3c3OXJqQjV1U0lUSUNVQTZHeWZ2U0ZmY3VJaXM3Qi9YRlNra3dBSFU1Uy9zMUFBUCtSMFRYN0hCV1VDNAogIHVhRzRXV3Npd0pLTm43bWdvM0F3YmpBT0JnTlZIUThCQWY4RUJBTUNBUVl3RWdZRFZSMFRBUUgvQkFnd0JnRUIKICAvd0lCQVRBZEJnTlZIUTRFRmdRVTVZdGpWVlBmZDdJN05MSHNuMkMyNkVCeUdWMHdLUVlEVlIwUkJDSXdJSUllCiAgYVdSbGJuUnBkSGt1YkdsdWEyVnlaQzVqYkhWemRHVnlMbXh2WTJGc01Bb0dDQ3FHU000OUJBTUNBMGtBTUVZQwogIElRQ043bEJGTEREdmp4NlYwK1hranBLRVJSc0pZZjVhZE12bmxvRmw0OGlsSmdJaEFOdHhobmRjcitRSlB1QzgKICB2Z1VDMGQyLzlGTXVlSVZNYis0NldUQ09qc3FyCiAgLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQpwb2xpY3lDb250cm9sbGVyOgogIGltYWdlOgogICAgdmVyc2lvbjogaW5zdGFsbC1jb250cm9sLXBsYW5lLXZlcnNpb24KcG9saWN5VmFsaWRhdG9yOgogIGNhQnVuZGxlOiBwb2xpY3kgdmFsaWRhdG9yIENBIGJ1bmRsZQogIGNydFBFTTogcG9saWN5IHZhbGlkYXRvciBjcnQKICBrZXlQRU06IHBvbGljeSB2YWxpZGF0b3Iga2V5CnByb2ZpbGVWYWxpZGF0b3I6CiAgY2FCdW5kbGU6IHByb2ZpbGUgdmFsaWRhdG9yIENBIGJ1bmRsZQogIGNydFBFTTogcHJvZmlsZSB2YWxpZGF0b3IgY3J0CiAga2V5UEVNOiBwcm9maWxlIHZhbGlkYXRvciBrZXkKcHJveHk6CiAgaW1hZ2U6CiAgICBuYW1lOiBteS5jdXN0b20ucmVnaXN0cnkvbGlua2VyZC1pby9wcm94eQogICAgdmVyc2lvbjogaW5zdGFsbC1wcm94eS12ZXJzaW9uCnByb3h5SW5pdDoKICBpbWFnZToKICAgIG5hbWU6IG15LmN1c3RvbS5yZWdpc3RyeS9saW5rZXJkLWlvL3Byb3h5LWluaXQKcHJveHlJbmplY3RvcjoKICBjYUJ1bmRsZTogcHJveHkgaW5qZWN0b3IgQ0EgYnVuZGxlCiAgY3J0UEVNOiBwcm94eSBpbmplY3RvciBjcnQKICBrZXlQRU06IHByb3h5IGluamVjdG9yIGtleQo=
+  linkerd-config-overrides: Y29udHJvbGxlckltYWdlOiBteS5jdXN0b20ucmVnaXN0cnkvbGlua2VyZC1pby9jb250cm9sbGVyCmNvbnRyb2xsZXJJbWFnZVZlcnNpb246IGluc3RhbGwtY29udHJvbC1wbGFuZS12ZXJzaW9uCmRlYnVnQ29udGFpbmVyOgogIGltYWdlOgogICAgbmFtZTogbXkuY3VzdG9tLnJlZ2lzdHJ5L2xpbmtlcmQtaW8vZGVidWcKICAgIHZlcnNpb246IGluc3RhbGwtZGVidWctdmVyc2lvbgpoZWFydGJlYXRTY2hlZHVsZTogMSAyIDMgNCA1CmlkZW50aXR5OgogIGlzc3VlcjoKICAgIGNydEV4cGlyeTogIjIwMzAtMDgtMjZUMDc6MTM6NDdaIgogICAgdGxzOgogICAgICBjcnRQRU06IHwKICAgICAgICAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KICAgICAgICBNSUlCd0RDQ0FXZWdBd0lCQWdJUkFKUklnWjhSdE84RXdnMVhlcGY4VDQ0d0NnWUlLb1pJemowRUF3SXdLVEVuCiAgICAgICAgTUNVR0ExVUVBeE1lYVdSbGJuUnBkSGt1YkdsdWEyVnlaQzVqYkhWemRHVnlMbXh2WTJGc01CNFhEVEl3TURneQogICAgICAgIE9EQTNNVE0wTjFvWERUTXdNRGd5TmpBM01UTTBOMW93S1RFbk1DVUdBMVVFQXhNZWFXUmxiblJwZEhrdWJHbHUKICAgICAgICBhMlZ5WkM1amJIVnpkR1Z5TG14dlkyRnNNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUxL0ZwCiAgICAgICAgZmNSbkRjZWRMNkFqVWFYWVB2NERJTUJhSnVmT0k1Tld0eStYU1g3SmpYZ1p0TTcyZFF2UmFZYW51eEQzNkR0MQogICAgICAgIDIvSnh5aVNneEtXUmRvYXkrYU53TUc0d0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUIKICAgICAgICBBZjhDQVFBd0hRWURWUjBPQkJZRUZJMVducnFNWUthSEhPbyt6cHlpaURxMnBPMEtNQ2tHQTFVZEVRUWlNQ0NDCiAgICAgICAgSG1sa1pXNTBhWFI1TG14cGJtdGxjbVF1WTJ4MWMzUmxjaTVzYjJOaGJEQUtCZ2dxaGtqT1BRUURBZ05IQURCRQogICAgICAgIEFpQXR1b0k1WHVDdHJHVlJ6U21SVGwycmEyOGFWOU15VFU3ZDVxblRBRkhLU2dJZ1JLQ3ZsdU9TZ0E1TzIxcDUKICAgICAgICA1MXRkcm1rSEVaUnIwcWxMU0pkSFlnRWZNems9CiAgICAgICAgLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQogICAgICBrZXlQRU06IHwKICAgICAgICAtLS0tLUJFR0lOIEVDIFBSSVZBVEUgS0VZLS0tLS0KICAgICAgICBNSGNDQVFFRUlBQWU4bmZielp1OWMvT0IyKzh4Sk0wRno3TlV3VFFhenVsa0ZOczRUSTUrb0FvR0NDcUdTTTQ5CiAgICAgICAgQXdFSG9VUURRZ0FFMS9GcGZjUm5EY2VkTDZBalVhWFlQdjRESU1CYUp1Zk9JNU5XdHkrWFNYN0pqWGdadE03MgogICAgICAgIGRRdlJhWWFudXhEMzZEdDEyL0p4eWlTZ3hLV1Jkb2F5K1E9PQogICAgICAgIC0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0KaWRlbnRpdHlUcnVzdEFuY2hvcnNQRU06IHwKICAtLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KICBNSUlCd1RDQ0FXYWdBd0lCQWdJUWVEWnA1bERhSXlnUTVVZk1LWnJGQVRBS0JnZ3Foa2pPUFFRREFqQXBNU2N3CiAgSlFZRFZRUURFeDVwWkdWdWRHbDBlUzVzYVc1clpYSmtMbU5zZFhOMFpYSXViRzlqWVd3d0hoY05NakF3T0RJNAogIE1EY3hNalEzV2hjTk16QXdPREkyTURjeE1qUTNXakFwTVNjd0pRWURWUVFERXg1cFpHVnVkR2wwZVM1c2FXNXIKICBaWEprTG1Oc2RYTjBaWEl1Ykc5allXd3dXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUnFjNzBaCiAgbDF2Z3c3OXJqQjV1U0lUSUNVQTZHeWZ2U0ZmY3VJaXM3Qi9YRlNra3dBSFU1Uy9zMUFBUCtSMFRYN0hCV1VDNAogIHVhRzRXV3Npd0pLTm43bWdvM0F3YmpBT0JnTlZIUThCQWY4RUJBTUNBUVl3RWdZRFZSMFRBUUgvQkFnd0JnRUIKICAvd0lCQVRBZEJnTlZIUTRFRmdRVTVZdGpWVlBmZDdJN05MSHNuMkMyNkVCeUdWMHdLUVlEVlIwUkJDSXdJSUllCiAgYVdSbGJuUnBkSGt1YkdsdWEyVnlaQzVqYkhWemRHVnlMbXh2WTJGc01Bb0dDQ3FHU000OUJBTUNBMGtBTUVZQwogIElRQ043bEJGTEREdmp4NlYwK1hranBLRVJSc0pZZjVhZE12bmxvRmw0OGlsSmdJaEFOdHhobmRjcitRSlB1QzgKICB2Z1VDMGQyLzlGTXVlSVZNYis0NldUQ09qc3FyCiAgLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQpwb2xpY3lDb250cm9sbGVyOgogIGltYWdlOgogICAgbmFtZTogbXkuY3VzdG9tLnJlZ2lzdHJ5L2xpbmtlcmQtaW8vcG9saWN5LWNvbnRyb2xsZXIKICAgIHZlcnNpb246IGluc3RhbGwtY29udHJvbC1wbGFuZS12ZXJzaW9uCnBvbGljeVZhbGlkYXRvcjoKICBjYUJ1bmRsZTogcG9saWN5IHZhbGlkYXRvciBDQSBidW5kbGUKICBjcnRQRU06IHBvbGljeSB2YWxpZGF0b3IgY3J0CiAga2V5UEVNOiBwb2xpY3kgdmFsaWRhdG9yIGtleQpwcm9maWxlVmFsaWRhdG9yOgogIGNhQnVuZGxlOiBwcm9maWxlIHZhbGlkYXRvciBDQSBidW5kbGUKICBjcnRQRU06IHByb2ZpbGUgdmFsaWRhdG9yIGNydAogIGtleVBFTTogcHJvZmlsZSB2YWxpZGF0b3Iga2V5CnByb3h5OgogIGltYWdlOgogICAgbmFtZTogbXkuY3VzdG9tLnJlZ2lzdHJ5L2xpbmtlcmQtaW8vcHJveHkKICAgIHZlcnNpb246IGluc3RhbGwtcHJveHktdmVyc2lvbgpwcm94eUluaXQ6CiAgaW1hZ2U6CiAgICBuYW1lOiBteS5jdXN0b20ucmVnaXN0cnkvbGlua2VyZC1pby9wcm94eS1pbml0CnByb3h5SW5qZWN0b3I6CiAgY2FCdW5kbGU6IHByb3h5IGluamVjdG9yIENBIGJ1bmRsZQogIGNydFBFTTogcHJveHkgaW5qZWN0b3IgY3J0CiAga2V5UEVNOiBwcm94eSBpbmplY3RvciBrZXkK
 kind: Secret
 metadata:
   creationTimestamp: null
diff --git a/jaeger/cmd/install_test.go b/jaeger/cmd/install_test.go
index e54daed8a..3c7e03b57 100644
--- a/jaeger/cmd/install_test.go
+++ b/jaeger/cmd/install_test.go
@@ -46,7 +46,7 @@ func TestRender(t *testing.T) {
 		t.Run(fmt.Sprintf("%d: %s", i, tc.goldenFileName), func(t *testing.T) {
 			var buf bytes.Buffer
 			// Merge overrides with default
-			if err := render(&buf, charts.MergeMaps(defaultValues, tc.values)); err != nil {
+			if err := render(&buf, charts.MergeMaps(defaultValues, tc.values), ""); err != nil {
 				t.Fatalf("Failed to render templates: %v", err)
 			}
 			testDataDiffer.DiffTestdata(t, tc.goldenFileName, buf.String())
diff --git a/test/integration/inject/inject_test.go b/test/integration/inject/inject_test.go
index de2fb644e..a63aca79d 100644
--- a/test/integration/inject/inject_test.go
+++ b/test/integration/inject/inject_test.go
@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme"
+	"github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/version"
 	"github.com/linkerd/linkerd2/testutil"
@@ -52,12 +53,16 @@ func parseDeployment(yamlString string) (*appsv1.Deployment, error) {
 }
 
 func TestInjectManualParams(t *testing.T) {
+	reg := "cr.l5d.io/linkerd"
+	if override := os.Getenv(flags.EnvOverrideDockerRegistry); reg != "" {
+		reg = override
+	}
 
 	injectionValidator := testutil.InjectValidator{
 		DisableIdentity:        true,
 		Version:                "proxy-version",
-		Image:                  "proxy-image",
-		InitImage:              "init-image",
+		Image:                  reg + "/proxy-image",
+		InitImage:              reg + "/init-image",
 		ImagePullPolicy:        "Never",
 		ControlPort:            123,
 		SkipInboundPorts:       "234,345",
@@ -380,9 +385,13 @@ func TestInjectAutoPod(t *testing.T) {
 	truthy := true
 	falsy := false
 	zero := int64(0)
+	reg := "cr.l5d.io/linkerd"
+	if override := os.Getenv(flags.EnvOverrideDockerRegistry); override != "" {
+		reg = override
+	}
 	expectedInitContainer := v1.Container{
 		Name:  k8s.InitContainerName,
-		Image: "cr.l5d.io/linkerd/proxy-init:" + version.ProxyInitVersion,
+		Image: reg + "/proxy-init:" + version.ProxyInitVersion,
 		Args: []string{
 			"--incoming-proxy-port", "4143",
 			"--outgoing-proxy-port", "4140",
diff --git a/test/integration/install_test.go b/test/integration/install_test.go
index 7bb75937b..a53929d5e 100644
--- a/test/integration/install_test.go
+++ b/test/integration/install_test.go
@@ -13,6 +13,8 @@ import (
 	"text/template"
 	"time"
 
+	"github.com/linkerd/linkerd2/pkg/cmd"
+	"github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/tls"
@@ -511,6 +513,24 @@ func helmOverridesEdge(root *tls.CA) ([]string, []string) {
 		"--set", "linkerdVersion=" + TestHelper.GetVersion(),
 		"--set", "namespace=" + TestHelper.GetVizNamespace(),
 	}
+
+	if override := os.Getenv(flags.EnvOverrideDockerRegistry); override != "" {
+		coreArgs = append(coreArgs,
+			"--set", "policyController.image.name="+cmd.RegistryOverride("cr.l5d.io/linkerd/policy-controller", override),
+			"--set", "proxy.image.name="+cmd.RegistryOverride("cr.l5d.io/linkerd/proxy", override),
+			"--set", "proxyInit.image.name="+cmd.RegistryOverride("cr.l5d.io/linkerd/proxy-init", override),
+			"--set", "controllerImage="+cmd.RegistryOverride("cr.l5d.io/linkerd/controller", override),
+			"--set", "debugContainer.image.name="+cmd.RegistryOverride("cr.l5d.io/linkerd/debug", override),
+		)
+		vizArgs = append(vizArgs,
+			"--set", "metricsAPI.image.registry="+override,
+			"--set", "tap.image.registry="+override,
+			"--set", "tapInjector.image.registry="+override,
+			"--set", "dashboard.image.registry="+override,
+			"--set", "grafana.image.registry="+override,
+		)
+	}
+
 	return coreArgs, vizArgs
 }
 
@@ -757,18 +777,40 @@ func TestOverridesSecret(t *testing.T) {
 		knownKeys := tree.Tree{
 			"controllerLogLevel": "debug",
 			"heartbeatSchedule":  "1 2 3 4 5",
-			"identity": map[string]interface{}{
-				"issuer": map[string]interface{}{},
+			"identity": tree.Tree{
+				"issuer": tree.Tree{},
 			},
 			"identityTrustAnchorsPEM": extractValue(t, "identityTrustAnchorsPEM"),
-			"proxyInit": map[string]interface{}{
+			"proxyInit": tree.Tree{
 				"ignoreInboundPorts": skippedInboundPorts,
 			},
 		}
 
+		if reg := os.Getenv(flags.EnvOverrideDockerRegistry); reg != "" {
+			knownKeys["controllerImage"] = reg + "/controller"
+			knownKeys["debugContainer"] = tree.Tree{
+				"image": tree.Tree{
+					"name": reg + "/debug",
+				},
+			}
+			knownKeys["policyController"] = tree.Tree{
+				"image": tree.Tree{
+					"name": reg + "/policy-controller",
+				},
+			}
+			knownKeys["proxy"] = tree.Tree{
+				"image": tree.Tree{
+					"name": reg + "/proxy",
+				},
+			}
+			knownKeys["proxyInit"].(tree.Tree)["image"] = tree.Tree{
+				"name": reg + "/proxy-init",
+			}
+		}
+
 		// Check for fields that were added during upgrade
 		if TestHelper.UpgradeFromVersion() != "" {
-			knownKeys["proxyInit"].(map[string]interface{})["ignoreOutboundPorts"] = skippedOutboundPorts
+			knownKeys["proxyInit"].(tree.Tree)["ignoreOutboundPorts"] = skippedOutboundPorts
 		}
 
 		if TestHelper.GetClusterDomain() != "cluster.local" {
@@ -776,13 +818,13 @@ func TestOverridesSecret(t *testing.T) {
 		}
 
 		if TestHelper.ExternalIssuer() {
-			knownKeys["identity"].(map[string]interface{})["issuer"].(map[string]interface{})["issuanceLifetime"] = "15s"
-			knownKeys["identity"].(map[string]interface{})["issuer"].(map[string]interface{})["scheme"] = "kubernetes.io/tls"
+			knownKeys["identity"].(tree.Tree)["issuer"].(tree.Tree)["issuanceLifetime"] = "15s"
+			knownKeys["identity"].(tree.Tree)["issuer"].(tree.Tree)["scheme"] = "kubernetes.io/tls"
 		} else {
 			if !TestHelper.Multicluster() {
-				knownKeys["identity"].(map[string]interface{})["issuer"].(map[string]interface{})["crtExpiry"] = extractValue(t, "identity", "issuer", "crtExpiry")
+				knownKeys["identity"].(tree.Tree)["issuer"].(tree.Tree)["crtExpiry"] = extractValue(t, "identity", "issuer", "crtExpiry")
 			}
-			knownKeys["identity"].(map[string]interface{})["issuer"].(map[string]interface{})["tls"] = map[string]interface{}{
+			knownKeys["identity"].(tree.Tree)["issuer"].(tree.Tree)["tls"] = tree.Tree{
 				"crtPEM": extractValue(t, "identity", "issuer", "tls", "crtPEM"),
 				"keyPEM": extractValue(t, "identity", "issuer", "tls", "keyPEM"),
 			}