Repository: linkerd/linkerd2
Pull request #4070 (2020-02-19 08:30:22)

Service mirror CLI 
Just putting this out here to encourage some feedback. This is based on the work that @adleong did around extracting credentials from remote clusters. I woner whether it makes sense to use go templates for rendering the credentials when creating them. Also, I wonder whether it is worth exposing a lot more parameters so you can pretty much override everything in your current k8s config context, instead of blindly copying it.

zaharidichev: @alpeb That is correct. Maybe I should put these clarification details in the cmd description
zaharidichev: @grampelberg There should be tests! I will add these once we agree on everything else. Wrt to the namespace, I think we can agree to add all of this into the linkerd namespace. This feels like a natural part of Linkerd so no need to differentiate (or at least no obvious to me reason). That however brings the question of where we should install the service mirroring controller. Right now its a separate helm chart with a seprate namespace. Should we leave it like that. Note that, it is perfectly fine for the mirroring controller to live in a separate ns than the credentials. Yet.. it feels a bit odd. If we follow the practice for the cni plugin, we probably want a separate namespace. Not sure.
grampelberg: > Right now its a separate helm chart with a seprate namespace. 

It should be a separate helm chart for sure. I don't see why we should stick it in a separate namespace though. At least, why don't we assume it is going into the linkerd control plane namespace for now and change it if someone complains.

My biggest piece of feedback is the naming, I just couldn't figure out what get/create were doing. I'm still trying to figure out what the right names should be though, not quite gotten my head around what the intent is there yet.
zaharidichev: @grampelberg @alpeb @adleong Okey so do we agree that everything goes into the control plane namespace (credentials, secrets, as well as the service mirror)?

My only argument is that we recently moved the CNI plugin into its own namespace. Why being inconsistent here?  
grampelberg: @zaharidichev I don't remember the context on that one. My preference is to keep things simple here until it doesn't make sense anymore.
zaharidichev: @grampelberg I think best things to do is what @alpeb suggested [right here ](https://github.com/linkerd/linkerd2/pull/4028#discussion_r382687454) and just leave the namespace out of the equation. People can specify their own namespace with via `kubectl`
grampelberg: @zaharidichev I was more concerned with all the flags in this PR around namespaces. I think that's a reasonable statement, but what are the implications here?
zaharidichev: @grampelberg The implications are that we do not specify anything namespace related. The commands simply output yamls. People will decide where to put these resources via their `kubectl`. Wouldn't that make sense?  And then.. there will be no need for namespace related flags in this PR. 
grampelberg: Let's do it, that sounds reasonable to me.
zaharidichev: @grampelberg @alpeb Because the `ClusterRoleBinding` needs to refer to a namespaced `ServiceAccount` in its `subject`, I have provided the `service-account-namespace` option which defaults to "default". I have not added a namespace to the secret that is output via the `get-credentials` command and users can add that wherever they want. 

Wrt to these commands being in seprate files, I do not think this is necessary. They are sufficiently small/simple to live in one file, much like he different stags of `check` for example (check config, et )

Wrt to tests, lets just agree that this looks gooas it is and I will write the tests along with golden files. 
grampelberg: > @grampelberg @alpeb Because the ClusterRoleBinding needs to refer to a namespaced ServiceAccount in its subject, I have provided the service-account-namespace option which defaults to "default". I have not added a namespace to the secret that is output via the get-credentials command and users can add that wherever they want.

This is fine for now, I definitely want to change it though. My preference is still to tie it to the linkerd namespace. `default` is definitely the wrong setting.
zaharidichev: @alpeb added more useful description + examples Feel free to take another look before I merge

Patch:
diff --git a/charts/linkerd2/values.yaml b/charts/linkerd2/values.yaml
index 575e773f3..b937e136a 100644
--- a/charts/linkerd2/values.yaml
+++ b/charts/linkerd2/values.yaml
@@ -170,7 +170,7 @@ nodeSelector:
   beta.kubernetes.io/os: linux
 
 smiMetrics:
-  image: deislabs/smi-metrics:v0.2.0
+  image: deislabs/smi-metrics:v0.2.1
   # if empty, Helm will auto-generate these fields
   crtPEM: |
 
diff --git a/cli/cmd/cluster.go b/cli/cmd/cluster.go
new file mode 100644
index 000000000..1b35566bd
--- /dev/null
+++ b/cli/cmd/cluster.go
@@ -0,0 +1,298 @@
+package cmd
+
+import (
+	"fmt"
+	"strings"
+
+	v1 "k8s.io/api/rbac/v1"
+
+	"github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/spf13/cobra"
+	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/yaml"
+
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/clientcmd/api"
+)
+
+const (
+	tokenKey                  = "token"
+	defaultServiceAccountName = "linkerd-service-mirror"
+	defaultServiceAccountNs   = "default"
+	defaultClusterName        = "remote"
+)
+
+type (
+	getCredentialsOptions struct {
+		namespace           string
+		serviceAccount      string
+		clusterName         string
+		remoteClusterDomain string
+	}
+
+	createOptions struct {
+		namespace      string
+		serviceAccount string
+	}
+
+	exportServiceOptions struct {
+		namespace        string
+		service          string
+		gatewayNamespace string
+		gatewayName      string
+	}
+)
+
+func newCmdCluster() *cobra.Command {
+
+	getOpts := getCredentialsOptions{}
+	createOpts := createOptions{}
+	exportOpts := exportServiceOptions{}
+	clusterCmd := &cobra.Command{
+
+		Hidden: true,
+		Use:    "cluster [flags]",
+		Args:   cobra.NoArgs,
+		Short:  "Manages the multicluster setup for Linkerd",
+		Long: `Manages the multicluster setup for Linkerd.
+
+This command provides subcommands to manage the multicluster support
+functionality of Linkerd. You can use it to deploy credentials to
+remote clusters, extract them as well as export remote services to be
+available across clusters.`,
+		Example: `  # Create remote cluster credentials.
+  linkerd --context=cluster-a cluster create-credentials | kubectl --context=cluster-a apply -f -
+
+  # Extract mirroring cluster credentials from cluster A and install them on cluster B
+  linkerd --context=cluster-a cluster get-credentials --cluster-name=remote | kubectl apply --context=cluster-b -f -
+
+  # Export service from cluster A to be available to other clusters
+  linkerd --context=cluster-a cluster export-service --service-name=backend-svc --service-namespace=default --gateway-name=linkerd-gateway --gateway-ns=default`,
+	}
+
+	createCredentialsCommand := &cobra.Command{
+		Hidden: false,
+		Use:    "create-credentials",
+		Short:  "Create the necessary credentials for service mirroring",
+		Args:   cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+
+			labels := map[string]string{
+				k8s.ControllerComponentLabel: k8s.ServiceMirrorLabel,
+				k8s.ControllerNSLabel:        controlPlaneNamespace,
+			}
+
+			clusterRole := v1.ClusterRole{
+				ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
+				TypeMeta:   metav1.TypeMeta{Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1"},
+				Rules: []rbacv1.PolicyRule{
+					{
+						APIGroups: []string{""},
+						Resources: []string{"services"},
+						Verbs:     []string{"list", "get", "watch"},
+					},
+				},
+			}
+
+			svcAccount := corev1.ServiceAccount{
+				ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
+				TypeMeta:   metav1.TypeMeta{Kind: v1.ServiceAccountKind, APIVersion: "v1"},
+			}
+
+			clusterRoleBinding := v1.ClusterRoleBinding{
+				TypeMeta:   metav1.TypeMeta{Kind: "ClusterRoleBinding", APIVersion: "rbac.authorization.k8s.io/v1"},
+				ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
+
+				Subjects: []v1.Subject{
+					v1.Subject{Kind: v1.ServiceAccountKind, Name: createOpts.serviceAccount, Namespace: createOpts.namespace},
+				},
+				RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", Name: createOpts.serviceAccount},
+			}
+
+			crOut, err := yaml.Marshal(clusterRole)
+			if err != nil {
+				return err
+			}
+
+			saOut, err := yaml.Marshal(svcAccount)
+			if err != nil {
+				return err
+			}
+			crbOut, err := yaml.Marshal(clusterRoleBinding)
+			if err != nil {
+				return err
+			}
+			fmt.Println(fmt.Sprintf("---\n%s---\n%s---\n%s", crOut, saOut, crbOut))
+			return nil
+		},
+	}
+
+	getCredentialsCmd := &cobra.Command{
+		Hidden: false,
+		Use:    "get-credentials",
+		Short:  "Get cluster credentials as a secret",
+		Args:   cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			rules := clientcmd.NewDefaultClientConfigLoadingRules()
+			rules.ExplicitPath = kubeconfigPath
+			loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
+			config, err := loader.RawConfig()
+			if err != nil {
+				return err
+			}
+
+			if kubeContext != "" {
+				config.CurrentContext = kubeContext
+			}
+
+			k, err := k8s.NewAPI(kubeconfigPath, config.CurrentContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return err
+			}
+
+			sa, err := k.CoreV1().ServiceAccounts(getOpts.namespace).Get(getOpts.serviceAccount, metav1.GetOptions{})
+			if err != nil {
+				return err
+			}
+
+			var secretName string
+			for _, s := range sa.Secrets {
+				if strings.HasPrefix(s.Name, fmt.Sprintf("%s-token", sa.Name)) {
+					secretName = s.Name
+					break
+				}
+			}
+			if secretName == "" {
+				return fmt.Errorf("could not find service account token secret for %s", sa.Name)
+			}
+
+			secret, err := k.CoreV1().Secrets(getOpts.namespace).Get(secretName, metav1.GetOptions{})
+			if err != nil {
+				return err
+			}
+
+			token, ok := secret.Data[tokenKey]
+			if !ok {
+				return fmt.Errorf("could not find the token data in the service account secret")
+			}
+
+			context, ok := config.Contexts[config.CurrentContext]
+			if !ok {
+				return fmt.Errorf("could not extract current context from config")
+			}
+
+			context.AuthInfo = getOpts.serviceAccount
+			config.Contexts = map[string]*api.Context{
+				config.CurrentContext: context,
+			}
+			config.AuthInfos = map[string]*api.AuthInfo{
+				getOpts.serviceAccount: {
+					Token: string(token),
+				},
+			}
+
+			cluster := config.Clusters[context.Cluster]
+
+			config.Clusters = map[string]*api.Cluster{
+				context.Cluster: cluster,
+			}
+
+			kubeconfig, err := clientcmd.Write(config)
+			if err != nil {
+				return err
+			}
+
+			creds := corev1.Secret{
+				Type:     k8s.MirrorSecretType,
+				TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
+				ObjectMeta: metav1.ObjectMeta{
+					Name: fmt.Sprintf("cluster-credentials-%s", getOpts.clusterName),
+					Annotations: map[string]string{
+						k8s.RemoteClusterNameLabel:        getOpts.clusterName,
+						k8s.RemoteClusterDomainAnnotation: getOpts.remoteClusterDomain,
+					},
+				},
+				Data: map[string][]byte{
+					k8s.ConfigKeyName: kubeconfig,
+				},
+			}
+
+			out, err := yaml.Marshal(creds)
+			if err != nil {
+				return err
+			}
+			fmt.Println(string(out))
+
+			return nil
+		},
+	}
+
+	exportServiceCmd := &cobra.Command{
+		Hidden: false,
+		Use:    "export-service",
+		Short:  "Exposes a remote service to be mirrored",
+		Args:   cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			rules := clientcmd.NewDefaultClientConfigLoadingRules()
+			rules.ExplicitPath = kubeconfigPath
+			loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
+			config, err := loader.RawConfig()
+			if err != nil {
+				return err
+			}
+
+			if kubeContext != "" {
+				config.CurrentContext = kubeContext
+			}
+
+			k, err := k8s.NewAPI(kubeconfigPath, config.CurrentContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return err
+			}
+
+			svc, err := k.CoreV1().Services(exportOpts.namespace).Get(exportOpts.service, metav1.GetOptions{})
+			if err != nil {
+				return err
+			}
+
+			_, hasGatewayName := svc.Annotations[k8s.GatewayNameAnnotation]
+			_, hasGatewayNs := svc.Annotations[k8s.GatewayNsAnnotation]
+
+			if hasGatewayName || hasGatewayNs {
+				return fmt.Errorf("service %s/%s has already been exported", svc.Namespace, svc.Name)
+			}
+
+			svc.Annotations[k8s.GatewayNameAnnotation] = exportOpts.gatewayName
+			svc.Annotations[k8s.GatewayNsAnnotation] = exportOpts.gatewayNamespace
+
+			_, err = k.CoreV1().Services(svc.Namespace).Update(svc)
+			if err != nil {
+				return err
+			}
+
+			fmt.Println(fmt.Sprintf("Service %s/%s is now exported", svc.Namespace, svc.Name))
+			return nil
+		},
+	}
+	getCredentialsCmd.Flags().StringVar(&getOpts.serviceAccount, "service-account-name", defaultServiceAccountName, "the name of the service account")
+	getCredentialsCmd.Flags().StringVar(&getOpts.namespace, "service-account-namespace", defaultServiceAccountNs, "the namespace in which the service account will be created")
+	getCredentialsCmd.Flags().StringVar(&getOpts.clusterName, "cluster-name", defaultClusterName, "cluster name")
+	getCredentialsCmd.Flags().StringVar(&getOpts.remoteClusterDomain, "remote-cluster-domain", defaultClusterDomain, "custom remote cluster domain")
+
+	createCredentialsCommand.Flags().StringVar(&createOpts.serviceAccount, "service-account-name", defaultServiceAccountName, "the name of the service account used")
+	createCredentialsCommand.Flags().StringVar(&createOpts.namespace, "service-account-namespace", defaultServiceAccountNs, "the namespace in which the service account can be found")
+
+	exportServiceCmd.Flags().StringVar(&exportOpts.service, "service-name", "", "the name of the service to be exported")
+	exportServiceCmd.Flags().StringVar(&exportOpts.namespace, "service-namespace", "", "the namespace in which the service to be exported resides")
+	exportServiceCmd.Flags().StringVar(&exportOpts.gatewayName, "gateway-name", "", "the name of the gateway")
+	exportServiceCmd.Flags().StringVar(&exportOpts.gatewayNamespace, "gateway-ns", "", "the ns of the gateway")
+
+	clusterCmd.AddCommand(getCredentialsCmd)
+	clusterCmd.AddCommand(createCredentialsCommand)
+	clusterCmd.AddCommand(exportServiceCmd)
+
+	return clusterCmd
+}
diff --git a/cli/cmd/root.go b/cli/cmd/root.go
index 0873e4839..317c00cc1 100644
--- a/cli/cmd/root.go
+++ b/cli/cmd/root.go
@@ -125,6 +125,7 @@ func init() {
 	RootCmd.AddCommand(newCmdUpgrade())
 	RootCmd.AddCommand(newCmdVersion())
 	RootCmd.AddCommand(newCmdInstallServiceMirror())
+	RootCmd.AddCommand(newCmdCluster())
 }
 
 type statOptionsBase struct {
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index 4a59499bb..ac8ebeba9 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -383,6 +383,9 @@ const (
 
 	// GatewayPortName is the name of the incoming port of the gateway
 	GatewayPortName = "incoming-port"
+
+	// ServiceMirrorLabel is the value used in the controller component label
+	ServiceMirrorLabel = "servicemirror"
 )
 
 // CreatedByAnnotationValue returns the value associated with


Test patch:
diff --git a/cli/cmd/testdata/install_control-plane.golden b/cli/cmd/testdata/install_control-plane.golden
index 4a2de2d3e..84a697581 100644
--- a/cli/cmd/testdata/install_control-plane.golden
+++ b/cli/cmd/testdata/install_control-plane.golden
@@ -2451,7 +2451,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index 62731a4e9..3c2b2d98a 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -3394,7 +3394,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index f3c1ff924..e35f36f35 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -3353,7 +3353,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 2d4bdf4f0..88e0e1dd2 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -3353,7 +3353,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index ded790e51..03f0b3b10 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -3617,7 +3617,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index ad9476571..2096b6c2e 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -3617,7 +3617,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 3f97e684e..c2fb601c1 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -3265,7 +3265,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_helm_output.golden b/cli/cmd/testdata/install_helm_output.golden
index ec4bf12b1..16b659f52 100644
--- a/cli/cmd/testdata/install_helm_output.golden
+++ b/cli/cmd/testdata/install_helm_output.golden
@@ -3220,7 +3220,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_helm_output_ha.golden b/cli/cmd/testdata/install_helm_output_ha.golden
index c24363898..c288aee05 100644
--- a/cli/cmd/testdata/install_helm_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_output_ha.golden
@@ -3484,7 +3484,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 2a3bc1239..baf2a4291 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -3053,7 +3053,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index ca6f160a0..6a25b7bb6 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -3344,7 +3344,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: ImagePullPolicy
 
         command:
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index 133433898..02882c944 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -3353,7 +3353,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_restricted_dashboard.golden b/cli/cmd/testdata/install_restricted_dashboard.golden
index 9bafc11b4..f76d46478 100644
--- a/cli/cmd/testdata/install_restricted_dashboard.golden
+++ b/cli/cmd/testdata/install_restricted_dashboard.golden
@@ -3288,7 +3288,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_tracing.golden b/cli/cmd/testdata/install_tracing.golden
index 66a63e3cb..a1902ee19 100644
--- a/cli/cmd/testdata/install_tracing.golden
+++ b/cli/cmd/testdata/install_tracing.golden
@@ -3353,7 +3353,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/upgrade_default.golden b/cli/cmd/testdata/upgrade_default.golden
index ec161c330..9de19b540 100644
--- a/cli/cmd/testdata/upgrade_default.golden
+++ b/cli/cmd/testdata/upgrade_default.golden
@@ -3362,7 +3362,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/upgrade_external_issuer.golden b/cli/cmd/testdata/upgrade_external_issuer.golden
index 60e32fa99..fca00a07a 100644
--- a/cli/cmd/testdata/upgrade_external_issuer.golden
+++ b/cli/cmd/testdata/upgrade_external_issuer.golden
@@ -3348,7 +3348,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/upgrade_ha.golden b/cli/cmd/testdata/upgrade_ha.golden
index ca96bdf74..872f7449b 100644
--- a/cli/cmd/testdata/upgrade_ha.golden
+++ b/cli/cmd/testdata/upgrade_ha.golden
@@ -3626,7 +3626,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/upgrade_overwrite_issuer.golden b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
index 9ef2a4c38..ea34d289c 100644
--- a/cli/cmd/testdata/upgrade_overwrite_issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
@@ -3353,7 +3353,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden b/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
index bd4bd8cd6..26bd483b1 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
@@ -3339,7 +3339,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
index 9ef2a4c38..ea34d289c 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
@@ -3353,7 +3353,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/pkg/charts/linkerd2/values_test.go b/pkg/charts/linkerd2/values_test.go
index 9ca4a649a..11c3f304b 100644
--- a/pkg/charts/linkerd2/values_test.go
+++ b/pkg/charts/linkerd2/values_test.go
@@ -125,7 +125,7 @@ func TestNewValues(t *testing.T) {
 		ProfileValidator: &ProfileValidator{TLS: &TLS{}},
 		Tap:              &Tap{TLS: &TLS{}},
 		SMIMetrics: &SMIMetrics{
-			Image: "deislabs/smi-metrics:v0.2.0",
+			Image: "deislabs/smi-metrics:v0.2.1",
 			TLS:   &TLS{},
 		},
 	}