Repository: linkerd/linkerd2
Pull request #4180 (2020-03-17 21:25:36)

Fix error when injecting Cronjobs that have no metadata
When injecting a Cronjob with no
`spec.jobTemplate.spec.template.metadata` we were getting the following
error:

```
Error transforming resources: jsonpatch add operation does not apply:
doc is missing path:
"/spec/jobTemplate/spec/template/metadata/annotations"
```

This only happens to Cronjobs because other workloads force having at
least a label there that is used in `spec.selector` (at least as of v1
workloads).

With this fix, if no metadata is detected, then we add it in the json patch when
injecting, prior to adding the injection annotation.

I've added a couple of new unit tests, one that verifies that this
doesn't remove metadata contents in Cronjobs that do have that metadata,
and another one that tests injection in Cronjobs that don't have
metadata (which I verified it failed prior to this fix).


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 29a408181..6b2cf3f0c 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -7,6 +7,8 @@
 
 run:
   deadline: 5m
+  skip-dirs:
+  - controller/gen
 issues:
   exclude-use-default: false
 linters:
diff --git a/CHANGES.md b/CHANGES.md
index a88cbb05c..9b9f442b0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,22 @@
+## edge-20.3.3
+
+This release introduces new experimental CLI commands for querying metrics using
+the Service Mesh Interface (SMI) and for multi-cluster support via service
+mirroring.
+
+If you would like to learn more about service mirroring or SMI, or are
+interested in experimenting with these features, please join us in
+[Linkerd Slack](https://slack.linkerd.io) for help and feedback.
+
+* CLI
+  * Added experimental `linkerd cluster` commands for managing multi-cluster
+    service mirroring
+  * Added the experimental `linkerd alpha clients` command, which uses the
+    smi-metrics API to display client-side metrics from each of a resource's
+    clients
+  * Added retries to some `linkerd check` checks to prevent spurious failures
+    when run immediately after cluster creation or Linkerd installation
+
 ## edge-20.3.2
 
 This release introduces substantial proxy improvements as well as
diff --git a/bin/create-release-tag b/bin/create-release-tag
index ab95ff9c4..0a36b8406 100755
--- a/bin/create-release-tag
+++ b/bin/create-release-tag
@@ -15,7 +15,7 @@ fi
 release_channel="$1"
 
 # Verify the release channel.
-release_channel_regex="(edge|stable)"
+release_channel_regex="^(edge|stable)$"
 if [[ ! $release_channel =~ $release_channel_regex ]]; then
     echo "Error: valid release channels: edge, stable"
     echo "Usage:"
diff --git a/bin/lint b/bin/lint
index 877646f02..1f89be279 100755
--- a/bin/lint
+++ b/bin/lint
@@ -2,7 +2,7 @@
 
 set -eu
 
-lintversion=1.19.1
+lintversion=1.23.8
 
 cd "$(pwd -P)"
 
diff --git a/charts/linkerd2/values.yaml b/charts/linkerd2/values.yaml
index 575e773f3..c58927922 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: |
 
@@ -181,15 +181,9 @@ tracing:
   enabled: false
   collector:
     name: linkerd-collector
-    image: omnition/opencensus-collector:0.1.10
+    image: omnition/opencensus-collector:0.1.11
     resources:
-      cpu:
-        limit: 1
-        request: 200m
-      memory:
-        limit: 2Gi
-        request: 400Mi
   jaeger:
     name: linkerd-jaeger
-    image: jaegertracing/all-in-one:1.8
+    image: jaegertracing/all-in-one:1.17.1
     resources:
diff --git a/charts/patch/templates/patch.json b/charts/patch/templates/patch.json
index 38864ec65..6f8703e95 100644
--- a/charts/patch/templates/patch.json
+++ b/charts/patch/templates/patch.json
@@ -1,5 +1,12 @@
 {{ $prefix := .Values.pathPrefix -}}
 [
+  {{- if .Values.addRootMetadata }}
+  {
+    "op": "add",
+    "path": "{{$prefix}}/metadata",
+    "value": {}
+  },
+  {{- end }}
   {{- if .Values.addRootAnnotations }}
   {
     "op": "add",
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/inject.go b/cli/cmd/inject.go
index c14d0be2c..3c10752de 100644
--- a/cli/cmd/inject.go
+++ b/cli/cmd/inject.go
@@ -300,7 +300,7 @@ func (resourceTransformerInject) generateReport(reports []inject.Report, output
 			if r.Kind != "" {
 				output.Write([]byte(fmt.Sprintf("%s \"%s\" skipped\n", r.Kind, r.Name)))
 			} else {
-				output.Write([]byte(fmt.Sprintf("document missing \"kind\" field, skipped\n")))
+				output.Write([]byte(fmt.Sprintln("document missing \"kind\" field, skipped")))
 			}
 		}
 	}
diff --git a/cli/cmd/root.go b/cli/cmd/root.go
index 0873e4839..326689be1 100644
--- a/cli/cmd/root.go
+++ b/cli/cmd/root.go
@@ -25,6 +25,8 @@ const (
 	jsonOutput  = "json"
 	tableOutput = "table"
 	wideOutput  = "wide"
+
+	maxRps = 100.0
 )
 
 var (
@@ -125,6 +127,7 @@ func init() {
 	RootCmd.AddCommand(newCmdUpgrade())
 	RootCmd.AddCommand(newCmdVersion())
 	RootCmd.AddCommand(newCmdInstallServiceMirror())
+	RootCmd.AddCommand(newCmdCluster())
 }
 
 type statOptionsBase struct {
diff --git a/cli/cmd/routes.go b/cli/cmd/routes.go
index d126fd5bf..04ad7d53a 100644
--- a/cli/cmd/routes.go
+++ b/cli/cmd/routes.go
@@ -19,9 +19,10 @@ import (
 
 type routesOptions struct {
 	statOptionsBase
-	toResource   string
-	toNamespace  string
-	dstIsService bool
+	toResource    string
+	toNamespace   string
+	dstIsService  bool
+	labelSelector string
 }
 
 type routeRowStats struct {
@@ -36,6 +37,7 @@ func newRoutesOptions() *routesOptions {
 		statOptionsBase: *newStatOptionsBase(),
 		toResource:      "",
 		toNamespace:     "",
+		labelSelector:   "",
 	}
 }
 
@@ -77,6 +79,7 @@ This command will only display traffic which is sent to a service that has a Ser
 	cmd.PersistentFlags().StringVar(&options.toResource, "to", options.toResource, "If present, shows outbound stats to the specified resource")
 	cmd.PersistentFlags().StringVar(&options.toNamespace, "to-namespace", options.toNamespace, "Sets the namespace used to lookup the \"--to\" resource; by default the current \"--namespace\" is used")
 	cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, fmt.Sprintf("Output format; one of: \"%s\", \"%s\", or \"%s\"", tableOutput, wideOutput, jsonOutput))
+	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='")
 
 	return cmd
 }
@@ -318,6 +321,7 @@ func buildTopRoutesRequest(resource string, options *routesOptions) (*pb.TopRout
 			ResourceType: target.Type,
 			Namespace:    options.namespace,
 		},
+		LabelSelector: options.labelSelector,
 	}
 
 	options.dstIsService = !(target.GetType() == k8s.Authority)
diff --git a/cli/cmd/tap.go b/cli/cmd/tap.go
index 56f259783..003c19d9a 100644
--- a/cli/cmd/tap.go
+++ b/cli/cmd/tap.go
@@ -23,15 +23,16 @@ import (
 type renderTapEventFunc func(*pb.TapEvent, string) string
 
 type tapOptions struct {
-	namespace   string
-	toResource  string
-	toNamespace string
-	maxRps      float32
-	scheme      string
-	method      string
-	authority   string
-	path        string
-	output      string
+	namespace     string
+	toResource    string
+	toNamespace   string
+	maxRps        float32
+	scheme        string
+	method        string
+	authority     string
+	path          string
+	output        string
+	labelSelector string
 }
 
 type endpoint struct {
@@ -102,15 +103,16 @@ type tapEvent struct {
 
 func newTapOptions() *tapOptions {
 	return &tapOptions{
-		namespace:   "default",
-		toResource:  "",
-		toNamespace: "",
-		maxRps:      100.0,
-		scheme:      "",
-		method:      "",
-		authority:   "",
-		path:        "",
-		output:      "",
+		namespace:     "default",
+		toResource:    "",
+		toNamespace:   "",
+		maxRps:        maxRps,
+		scheme:        "",
+		method:        "",
+		authority:     "",
+		path:          "",
+		output:        "",
+		labelSelector: "",
 	}
 }
 
@@ -169,16 +171,17 @@ func newCmdTap() *cobra.Command {
 		ValidArgs: util.ValidTargets,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			requestParams := util.TapRequestParams{
-				Resource:    strings.Join(args, "/"),
-				Namespace:   options.namespace,
-				ToResource:  options.toResource,
-				ToNamespace: options.toNamespace,
-				MaxRps:      options.maxRps,
-				Scheme:      options.scheme,
-				Method:      options.method,
-				Authority:   options.authority,
-				Path:        options.path,
-				Extract:     options.output == jsonOutput,
+				Resource:      strings.Join(args, "/"),
+				Namespace:     options.namespace,
+				ToResource:    options.toResource,
+				ToNamespace:   options.toNamespace,
+				MaxRps:        options.maxRps,
+				Scheme:        options.scheme,
+				Method:        options.method,
+				Authority:     options.authority,
+				Path:          options.path,
+				Extract:       options.output == jsonOutput,
+				LabelSelector: options.labelSelector,
 			}
 
 			err := options.validate()
@@ -218,6 +221,8 @@ func newCmdTap() *cobra.Command {
 		"Display requests with paths that start with this prefix")
 	cmd.PersistentFlags().StringVarP(&options.output, "output", "o", options.output,
 		fmt.Sprintf("Output format. One of: \"%s\", \"%s\"", wideOutput, jsonOutput))
+	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector,
+		"Selector (label query) to filter on, supports '=', '==', and '!='")
 
 	return cmd
 }
diff --git a/cli/cmd/top.go b/cli/cmd/top.go
index 0708ee3c6..f1532a7e8 100644
--- a/cli/cmd/top.go
+++ b/cli/cmd/top.go
@@ -25,16 +25,17 @@ import (
 )
 
 type topOptions struct {
-	namespace   string
-	toResource  string
-	toNamespace string
-	maxRps      float32
-	scheme      string
-	method      string
-	authority   string
-	path        string
-	hideSources bool
-	routes      bool
+	namespace     string
+	toResource    string
+	toNamespace   string
+	maxRps        float32
+	scheme        string
+	method        string
+	authority     string
+	path          string
+	hideSources   bool
+	routes        bool
+	labelSelector string
 }
 
 type topRequest struct {
@@ -258,16 +259,17 @@ const (
 
 func newTopOptions() *topOptions {
 	return &topOptions{
-		namespace:   "default",
-		toResource:  "",
-		toNamespace: "",
-		maxRps:      100.0,
-		scheme:      "",
-		method:      "",
-		authority:   "",
-		path:        "",
-		hideSources: false,
-		routes:      false,
+		namespace:     "default",
+		toResource:    "",
+		toNamespace:   "",
+		maxRps:        maxRps,
+		scheme:        "",
+		method:        "",
+		authority:     "",
+		path:          "",
+		hideSources:   false,
+		routes:        false,
+		labelSelector: "",
 	}
 }
 
@@ -317,15 +319,16 @@ func newCmdTop() *cobra.Command {
 		ValidArgs: util.ValidTargets,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			requestParams := util.TapRequestParams{
-				Resource:    strings.Join(args, "/"),
-				Namespace:   options.namespace,
-				ToResource:  options.toResource,
-				ToNamespace: options.toNamespace,
-				MaxRps:      options.maxRps,
-				Scheme:      options.scheme,
-				Method:      options.method,
-				Authority:   options.authority,
-				Path:        options.path,
+				Resource:      strings.Join(args, "/"),
+				Namespace:     options.namespace,
+				ToResource:    options.toResource,
+				ToNamespace:   options.toNamespace,
+				MaxRps:        options.maxRps,
+				Scheme:        options.scheme,
+				Method:        options.method,
+				Authority:     options.authority,
+				Path:          options.path,
+				LabelSelector: options.labelSelector,
 			}
 
 			if options.hideSources {
@@ -374,6 +377,7 @@ func newCmdTop() *cobra.Command {
 		"Display requests with paths that start with this prefix")
 	cmd.PersistentFlags().BoolVar(&options.hideSources, "hide-sources", options.hideSources, "Hide the source column")
 	cmd.PersistentFlags().BoolVar(&options.routes, "routes", options.routes, "Display data per route instead of per path")
+	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='")
 
 	return cmd
 }
diff --git a/cli/cmd/uninject.go b/cli/cmd/uninject.go
index a88c7dc89..d82e5ad23 100644
--- a/cli/cmd/uninject.go
+++ b/cli/cmd/uninject.go
@@ -96,7 +96,7 @@ func (resourceTransformerUninject) generateReport(reports []inject.Report, outpu
 			if r.Kind != "" {
 				output.Write([]byte(fmt.Sprintf("%s \"%s\" skipped\n", r.Kind, r.Name)))
 			} else {
-				output.Write([]byte(fmt.Sprintf("document missing \"kind\" field, skipped\n")))
+				output.Write([]byte(fmt.Sprintln("document missing \"kind\" field, skipped")))
 			}
 		}
 	}
diff --git a/controller/api/public/top_routes.go b/controller/api/public/top_routes.go
index dcd5e34f0..ba21ee51b 100644
--- a/controller/api/public/top_routes.go
+++ b/controller/api/public/top_routes.go
@@ -53,7 +53,10 @@ func (s *grpcServer) TopRoutes(ctx context.Context, req *pb.TopRoutesRequest) (*
 	// TopRoutes will return one table for each resource object requested.
 	tables := make([]resourceTable, 0)
 	targetResource := req.GetSelector().GetResource()
-
+	labelSelector, err := getTopLabelSelector(req)
+	if err != nil {
+		return nil, err
+	}
 	if targetResource.GetType() == k8s.Authority {
 		// Authority cannot be the target because authorities don't have namespaces,
 		// therefore there is no namespace in which to look for a service profile.
@@ -61,7 +64,7 @@ func (s *grpcServer) TopRoutes(ctx context.Context, req *pb.TopRoutesRequest) (*
 	}
 
 	// Non-authority resource
-	objects, err := s.k8sAPI.GetObjects(targetResource.Namespace, targetResource.Type, targetResource.Name, labels.Everything())
+	objects, err := s.k8sAPI.GetObjects(targetResource.Namespace, targetResource.Type, targetResource.Name, labelSelector)
 	if err != nil {
 		return nil, err
 	}
@@ -114,6 +117,10 @@ func (s *grpcServer) topRoutesFor(ctx context.Context, req *pb.TopRoutesRequest,
 	}
 	clientNs := req.GetSelector().GetResource().GetNamespace()
 	typ := req.GetSelector().GetResource().GetType()
+	labelSelector, err := getTopLabelSelector(req)
+	if err != nil {
+		return nil, err
+	}
 	targetResource := &pb.Resource{
 		Name:      name,
 		Namespace: req.GetSelector().GetResource().GetNamespace(),
@@ -128,14 +135,14 @@ func (s *grpcServer) topRoutesFor(ctx context.Context, req *pb.TopRoutesRequest,
 
 	if requestedResource.GetType() == k8s.Authority {
 		// Authorities may not be a source, so we know this is a ToResource.
-		profiles, err = s.getProfilesForAuthority(requestedResource.GetName(), clientNs)
+		profiles, err = s.getProfilesForAuthority(requestedResource.GetName(), clientNs, labelSelector)
 		if err != nil {
 			return nil, err
 		}
 	} else {
 		// Non-authority resource.
 		// Lookup individual resource objects.
-		objects, err := s.k8sAPI.GetObjects(requestedResource.Namespace, requestedResource.Type, requestedResource.Name, labels.Everything())
+		objects, err := s.k8sAPI.GetObjects(requestedResource.Namespace, requestedResource.Type, requestedResource.Name, labelSelector)
 		if err != nil {
 			return nil, err
 		}
@@ -191,10 +198,10 @@ func validateRequest(req *pb.TopRoutesRequest) *pb.TopRoutesResponse {
 	return nil
 }
 
-func (s *grpcServer) getProfilesForAuthority(authority string, clientNs string) (map[string]*sp.ServiceProfile, error) {
+func (s *grpcServer) getProfilesForAuthority(authority string, clientNs string, labelSelector labels.Selector) (map[string]*sp.ServiceProfile, error) {
 	if authority == "" {
 		// All authorities
-		ps, err := s.k8sAPI.SP().Lister().ServiceProfiles(clientNs).List(labels.Everything())
+		ps, err := s.k8sAPI.SP().Lister().ServiceProfiles(clientNs).List(labelSelector)
 		if err != nil {
 			return nil, err
 		}
@@ -347,3 +354,16 @@ func processRouteMetrics(results []promResult, timeWindow string, table indexedT
 		}
 	}
 }
+
+//generate correct label.Selector object according to the request
+func getTopLabelSelector(req *pb.TopRoutesRequest) (labels.Selector, error) {
+	labelSelector := labels.Everything()
+	if s := req.GetSelector().GetLabelSelector(); s != "" {
+		var err error
+		labelSelector, err = labels.Parse(s)
+		if err != nil {
+			return nil, fmt.Errorf("invalid label selector \"%s\": %s", s, err)
+		}
+	}
+	return labelSelector, nil
+}
diff --git a/controller/api/util/api_utils.go b/controller/api/util/api_utils.go
index 67321497d..0a11879ee 100644
--- a/controller/api/util/api_utils.go
+++ b/controller/api/util/api_utils.go
@@ -94,24 +94,26 @@ type EdgesRequestParams struct {
 // requests.
 type TopRoutesRequestParams struct {
 	StatsBaseRequestParams
-	ToNamespace string
-	ToType      string
-	ToName      string
+	ToNamespace   string
+	ToType        string
+	ToName        string
+	LabelSelector string
 }
 
 // TapRequestParams contains parameters that are used to build a
 // TapByResourceRequest.
 type TapRequestParams struct {
-	Resource    string
-	Namespace   string
-	ToResource  string
-	ToNamespace string
-	MaxRps      float32
-	Scheme      string
-	Method      string
-	Authority   string
-	Path        string
-	Extract     bool
+	Resource      string
+	Namespace     string
+	ToResource    string
+	ToNamespace   string
+	MaxRps        float32
+	Scheme        string
+	Method        string
+	Authority     string
+	Path          string
+	Extract       bool
+	LabelSelector string
 }
 
 // GRPCError generates a gRPC error code, as defined in
@@ -301,6 +303,7 @@ func BuildTopRoutesRequest(p TopRoutesRequestParams) (*pb.TopRoutesRequest, erro
 				Name:      p.ResourceName,
 				Type:      resourceType,
 			},
+			LabelSelector: p.LabelSelector,
 		},
 		TimeWindow: window,
 	}
@@ -513,7 +516,8 @@ func BuildTapByResourceRequest(params TapRequestParams) (*pb.TapByResourceReques
 
 	return &pb.TapByResourceRequest{
 		Target: &pb.ResourceSelection{
-			Resource: &target,
+			Resource:      &target,
+			LabelSelector: params.LabelSelector,
 		},
 		MaxRps: params.MaxRps,
 		Match: &pb.TapByResourceRequest_Match{
diff --git a/controller/tap/server.go b/controller/tap/server.go
index dab120eaa..607da7cec 100644
--- a/controller/tap/server.go
+++ b/controller/tap/server.go
@@ -59,6 +59,10 @@ func (s *GRPCTapServer) TapByResource(req *public.TapByResourceRequest, stream p
 		return status.Error(codes.InvalidArgument, "TapByResource received nil target ResourceSelection")
 	}
 	res := req.GetTarget().GetResource()
+	labelSelector, err := getLabelSelector(req)
+	if err != nil {
+		return err
+	}
 	if res == nil {
 		return status.Error(codes.InvalidArgument, "TapByResource received nil target Resource")
 	}
@@ -66,7 +70,7 @@ func (s *GRPCTapServer) TapByResource(req *public.TapByResourceRequest, stream p
 		req.MaxRps = defaultMaxRps
 	}
 
-	objects, err := s.k8sAPI.GetObjects(res.GetNamespace(), res.GetType(), res.GetName(), labels.Everything())
+	objects, err := s.k8sAPI.GetObjects(res.GetNamespace(), res.GetType(), res.GetName(), labelSelector)
 	if err != nil {
 		return apiUtil.GRPCError(err)
 	}
@@ -651,3 +655,15 @@ func (s *GRPCTapServer) resourceForIP(ip *public.IPAddress) (runtime.Object, err
 
 	return singleRunningPod, nil
 }
+
+func getLabelSelector(req *public.TapByResourceRequest) (labels.Selector, error) {
+	labelSelector := labels.Everything()
+	if s := req.GetTarget().GetLabelSelector(); s != "" {
+		var err error
+		labelSelector, err = labels.Parse(s)
+		if err != nil {
+			return nil, fmt.Errorf("invalid label selector \"%s\": %s", s, err)
+		}
+	}
+	return labelSelector, nil
+}
diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go
index b2e862368..9c7bae54a 100644
--- a/pkg/inject/inject.go
+++ b/pkg/inject/inject.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"reflect"
 	"regexp"
 	"sort"
 	"strconv"
@@ -117,6 +118,7 @@ type ResourceConfig struct {
 type patch struct {
 	l5dcharts.Values
 	PathPrefix            string                    `json:"pathPrefix"`
+	AddRootMetadata       bool                      `json:"addRootMetadata"`
 	AddRootAnnotations    bool                      `json:"addRootAnnotations"`
 	Annotations           map[string]string         `json:"annotations"`
 	AddRootLabels         bool                      `json:"addRootLabels"`
@@ -641,6 +643,12 @@ func (conf *ResourceConfig) injectObjectMeta(values *patch) {
 }
 
 func (conf *ResourceConfig) injectPodAnnotations(values *patch) {
+	// ObjectMetaAnnotations.Annotations is nil for new empty structs, but we always initialize
+	// it to an empty map in parse() above, so we follow suit here.
+	emptyMeta := &metav1.ObjectMeta{Annotations: map[string]string{}}
+	// Cronjobs in batch/v1beta1 might have an empty `spec.jobTemplate.spec.template.metadata`
+	// field so we make sure to create it if needed, before attempting adding annotations
+	values.AddRootMetadata = reflect.DeepEqual(conf.pod.meta, emptyMeta)
 	values.AddRootAnnotations = len(conf.pod.meta.Annotations) == 0
 
 	for _, k := range sortedKeys(conf.pod.annotations) {
diff --git a/pkg/k8s/fake.go b/pkg/k8s/fake.go
index 8c6547e94..36591b7b2 100644
--- a/pkg/k8s/fake.go
+++ b/pkg/k8s/fake.go
@@ -115,6 +115,7 @@ func NewFakeClientSets(configs ...string) (
 // newFakeClientSetsFromManifests reads from a slice of readers, each
 // representing a manifest or collection of manifests, and returns a mock
 // Kubernetes ClientSet.
+//nolint:unparam
 func newFakeClientSetsFromManifests(readers []io.Reader) (
 	kubernetes.Interface,
 	apiextensionsclient.Interface,
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
diff --git a/pkg/profiles/proto.go b/pkg/profiles/proto.go
index a36a12eb0..0255b3c83 100644
--- a/pkg/profiles/proto.go
+++ b/pkg/profiles/proto.go
@@ -45,11 +45,18 @@ func protoToServiceProfile(parser *proto.Parser, namespace, name, clusterDomain
 			pkg = typed.Name
 		case *proto.RPC:
 			if service, ok := typed.Parent.(*proto.Service); ok {
+				var path string
+				switch pkg {
+				case "":
+					path = fmt.Sprintf("/%s/%s", service.Name, typed.Name)
+				default:
+					path = fmt.Sprintf("/%s.%s/%s", pkg, service.Name, typed.Name)
+				}
 				route := &sp.RouteSpec{
 					Name: typed.Name,
 					Condition: &sp.RequestMatch{
 						Method:    http.MethodPost,
-						PathRegex: regexp.QuoteMeta(fmt.Sprintf("/%s.%s/%s", pkg, service.Name, typed.Name)),
+						PathRegex: regexp.QuoteMeta(path),
 					},
 				}
 				routes = append(routes, route)
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index 7137e6dc4..8c5865cc5 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -1596,9 +1596,9 @@ acorn-walk@^6.0.1, acorn-walk@^6.1.1:
   integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
 
 acorn@^5.5.3:
-  version "5.7.3"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
-  integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
+  version "5.7.4"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
+  integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
 
 acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.1:
   version "6.3.0"


Test patch:
diff --git a/cli/cmd/inject_test.go b/cli/cmd/inject_test.go
index 946c99c9b..ed7f2e871 100644
--- a/cli/cmd/inject_test.go
+++ b/cli/cmd/inject_test.go
@@ -205,6 +205,20 @@ func TestUninjectAndInject(t *testing.T) {
 			injectProxy:      true,
 			testInjectConfig: defaultConfig,
 		},
+		{
+			inputFileName:    "inject_emojivoto_cronjob.input.yml",
+			goldenFileName:   "inject_emojivoto_cronjob.golden.yml",
+			reportFileName:   "inject_emojivoto_cronjob.report",
+			injectProxy:      false,
+			testInjectConfig: defaultConfig,
+		},
+		{
+			inputFileName:    "inject_emojivoto_cronjob_nometa.input.yml",
+			goldenFileName:   "inject_emojivoto_cronjob_nometa.golden.yml",
+			reportFileName:   "inject_emojivoto_cronjob.report",
+			injectProxy:      false,
+			testInjectConfig: defaultConfig,
+		},
 		{
 			inputFileName:    "inject_emojivoto_pod.input.yml",
 			goldenFileName:   "inject_emojivoto_pod.golden.yml",
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob.golden.yml b/cli/cmd/testdata/inject_emojivoto_cronjob.golden.yml
new file mode 100644
index 000000000..3fb2469b6
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob.golden.yml
@@ -0,0 +1,25 @@
+apiVersion: batch/v1beta1
+kind: CronJob
+metadata:
+  name: hello
+  namespace: emojivoto
+spec:
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          annotations:
+            linkerd.io/inject: enabled
+          labels:
+            foo: bar
+        spec:
+          containers:
+          - args:
+            - /bin/sh
+            - -c
+            - date; echo Hello from the Kubernetes cluster
+            image: busybox
+            name: hello
+          restartPolicy: OnFailure
+  schedule: '*/10 * * * *'
+---
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob.input.yml b/cli/cmd/testdata/inject_emojivoto_cronjob.input.yml
new file mode 100644
index 000000000..1990e26a1
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob.input.yml
@@ -0,0 +1,22 @@
+apiVersion: batch/v1beta1
+kind: CronJob
+metadata:
+  namespace: emojivoto
+  name: hello
+spec:
+  schedule: "*/10 * * * *"
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          labels:
+            foo: bar
+        spec:
+          containers:
+          - name: hello
+            image: busybox
+            args:
+            - /bin/sh
+            - -c
+            - date; echo Hello from the Kubernetes cluster
+          restartPolicy: OnFailure
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob.report b/cli/cmd/testdata/inject_emojivoto_cronjob.report
new file mode 100644
index 000000000..7917ed58d
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob.report
@@ -0,0 +1,3 @@
+
+cronjob "hello" injected
+
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob.report.verbose b/cli/cmd/testdata/inject_emojivoto_cronjob.report.verbose
new file mode 100644
index 000000000..de6f69c5e
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob.report.verbose
@@ -0,0 +1,9 @@
+
+√ pods do not use host networking
+√ pods do not have a 3rd party proxy or initContainer already injected
+√ pods are not annotated to disable injection
+√ at least one resource injected
+√ pod specs do not include UDP ports
+
+cronjob "hello" injected
+
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.golden.yml b/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.golden.yml
new file mode 100644
index 000000000..fd618b72f
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.golden.yml
@@ -0,0 +1,23 @@
+apiVersion: batch/v1beta1
+kind: CronJob
+metadata:
+  name: hello
+  namespace: emojivoto
+spec:
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          annotations:
+            linkerd.io/inject: enabled
+        spec:
+          containers:
+          - args:
+            - /bin/sh
+            - -c
+            - date; echo Hello from the Kubernetes cluster
+            image: busybox
+            name: hello
+          restartPolicy: OnFailure
+  schedule: '*/10 * * * *'
+---
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.input.yml b/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.input.yml
new file mode 100644
index 000000000..432691450
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.input.yml
@@ -0,0 +1,19 @@
+apiVersion: batch/v1beta1
+kind: CronJob
+metadata:
+  namespace: emojivoto
+  name: hello
+spec:
+  schedule: "*/10 * * * *"
+  jobTemplate:
+    spec:
+      template:
+        spec:
+          containers:
+          - name: hello
+            image: busybox
+            args:
+            - /bin/sh
+            - -c
+            - date; echo Hello from the Kubernetes cluster
+          restartPolicy: OnFailure
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..e547b4981 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:
@@ -3544,17 +3544,11 @@ data:
     tracing:
       enabled: true
       collector:
-        image: omnition/opencensus-collector:0.1.10
+        image: omnition/opencensus-collector:0.1.11
         name: linkerd-collector
-        resources:
-          cpu:
-            limit: 1
-            request: 200m
-          memory:
-            limit: 2Gi
-            request: 400Mi
+        resources: null
       jaeger:
-        image: jaegertracing/all-in-one:1.8
+        image: jaegertracing/all-in-one:1.17.1
         name: linkerd-jaeger
         resources: null
 ---
@@ -3682,7 +3676,7 @@ spec:
         env:
         - name: GOGC
           value: "80"
-        image: omnition/opencensus-collector:0.1.10
+        image: omnition/opencensus-collector:0.1.11
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
@@ -3696,13 +3690,6 @@ spec:
           httpGet:
             path: /
             port: 13133
-        resources:
-          limits:
-            cpu: "1"
-            memory: "2Gi"
-          requests:
-            cpu: "200m"
-            memory: "400Mi"
         volumeMounts:
         - mountPath: /conf
           name: linkerd-collector-config-val
@@ -3898,7 +3885,7 @@ spec:
         linkerd.io/proxy-deployment: linkerd-jaeger
     spec:
       containers:
-      - image: jaegertracing/all-in-one:1.8
+      - image: jaegertracing/all-in-one:1.17.1
         imagePullPolicy: IfNotPresent
         name: jaeger
         ports:
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{},
 		},
 	}