Repository: linkerd/linkerd2
Pull request #11892 (2024-01-07 17:14:22)

build(deps): bump date-fns from 3.0.6 to 3.1.0 in /web/app
Bumps [date-fns](https://github.com/date-fns/date-fns) from 3.0.6 to 3.1.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/date-fns/date-fns/releases">date-fns's releases</a>.</em></p>
<blockquote>
<h2>v3.1.0</h2>
<p>This release is brought to you by <a href="https://github.com/kossnocorp"><code>@​kossnocorp</code></a>, <a href="https://github.com/makstyle119"><code>@​makstyle119</code></a> and <a href="https://github.com/dmgawel"><code>@​dmgawel</code></a>.</p>
<h3>Fixed</h3>
<ul>
<li><a href="https://redirect.github.com/date-fns/date-fns/pull/3448">Fixed the plural form of weeks in Swedish</a>.</li>
</ul>
<h3>Added</h3>
<ul>
<li>
<p><a href="https://redirect.github.com/date-fns/date-fns/pull/3540">Added <code>yearsToDays</code> function</a>.</p>
</li>
<li>
<p>Added a warning about using protected tokens like <code>Y</code> or <code>D</code> without passing a corresponding option. [See <a href="https://redirect.github.com/date-fns/date-fns/issues/2950">#2950</a>](<a href="https://redirect.github.com/date-fns/date-fns/issues/2950">date-fns/date-fns#2950</a>).</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/date-fns/date-fns/blob/main/CHANGELOG.md">date-fns's changelog</a>.</em></p>
<blockquote>
<h2>v3.1.0 - 2024-01-05</h2>
<p>This release is brought to you by <a href="https://github.com/kossnocorp"><code>@​kossnocorp</code></a>, <a href="https://github.com/makstyle119"><code>@​makstyle119</code></a> and <a href="https://github.com/dmgawel"><code>@​dmgawel</code></a>.</p>
<h3>Fixed</h3>
<ul>
<li><a href="https://redirect.github.com/date-fns/date-fns/pull/3448">Fixed the plural form of weeks in Swedish</a>.</li>
</ul>
<h3>Added</h3>
<ul>
<li>
<p><a href="https://redirect.github.com/date-fns/date-fns/pull/3540">Added <code>yearsToDays</code> function</a>.</p>
</li>
<li>
<p>Added warning about using protected tokens like <code>Y</code> or <code>D</code> without passing a corresponding option. [See <a href="https://redirect.github.com/date-fns/date-fns/issues/2950">#2950</a>](<a href="https://redirect.github.com/date-fns/date-fns/issues/2950">date-fns/date-fns#2950</a>).</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/date-fns/date-fns/commit/9a158ddfd35a591449fff5a27d752b38406a6456"><code>9a158dd</code></a> Promote to v3.1.0</li>
<li><a href="https://github.com/date-fns/date-fns/commit/651d47b2756fd88f3786ccd96fbae09d5830e365"><code>651d47b</code></a> Fix yearsToDays</li>
<li><a href="https://github.com/date-fns/date-fns/commit/127775bc885241cfbbc309e82903bedf558b33da"><code>127775b</code></a> Build the library</li>
<li><a href="https://github.com/date-fns/date-fns/commit/2b689dd4b5a491468ae2e8a7b49f5a148314f7ac"><code>2b689dd</code></a> Add yearsToDays Function (<a href="https://redirect.github.com/date-fns/date-fns/issues/3540">#3540</a>)</li>
<li><a href="https://github.com/date-fns/date-fns/commit/a576d6ebb78341fb46b8c32c784c8577fa82aac4"><code>a576d6e</code></a> Warn about using protected tokens like Y or D</li>
<li><a href="https://github.com/date-fns/date-fns/commit/5a043dc5d5b57be34649e4b4a18023e459c788ff"><code>5a043dc</code></a> Fix plural form of weeks in Swedish (<a href="https://redirect.github.com/date-fns/date-fns/issues/3448">#3448</a>)</li>
<li><a href="https://github.com/date-fns/date-fns/commit/a39b8058f777517e8040193ba597dff18765951a"><code>a39b805</code></a> Update outdated docs on rounding</li>
<li>See full diff in <a href="https://github.com/date-fns/date-fns/compare/v3.0.6...v3.1.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=date-fns&package-manager=npm_and_yarn&previous-version=3.0.6&new-version=3.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

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


</details>


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 1f47adcfc..64471137b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -668,11 +668,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
 
 [[package]]
 name = "home"
-version = "0.5.5"
+version = "0.5.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
 dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
diff --git a/bin/update-codegen.sh b/bin/update-codegen.sh
index 0f6e591ba..522413845 100755
--- a/bin/update-codegen.sh
+++ b/bin/update-codegen.sh
@@ -13,7 +13,7 @@ CODEGEN_PKG=${GOPATH}/pkg/mod/k8s.io/code-generator@${GEN_VER}
 rm -rf "${SCRIPT_ROOT}/controller/gen/client/clientset/*"
 rm -rf "${SCRIPT_ROOT}/controller/gen/client/listeners/*"
 rm -rf "${SCRIPT_ROOT}/controller/gen/client/informers/*"
-crds=(serviceprofile:v1alpha2 server:v1beta1 serverauthorization:v1beta1 link:v1alpha1 policy:v1alpha1 policy:v1beta3)
+crds=(serviceprofile:v1alpha2 server:v1beta1 serverauthorization:v1beta1 link:v1alpha1 policy:v1alpha1 policy:v1beta3 externalworkload:v1alpha1)
 for crd in "${crds[@]}"
 do
   crd_path=$(tr : / <<< "$crd")
diff --git a/charts/linkerd-crds/templates/policy/server.yaml b/charts/linkerd-crds/templates/policy/server.yaml
index a5d9f26da..ac3215c02 100644
--- a/charts/linkerd-crds/templates/policy/server.yaml
+++ b/charts/linkerd-crds/templates/policy/server.yaml
@@ -72,7 +72,9 @@ spec:
                   default: unknown
     - name: v1beta1
       served: true
-      storage: true
+      storage: false
+      deprecated: true
+      deprecationWarning: "policy.linkerd.io/v1alpha1 Server is deprecated; use policy.linkerd.io/v1beta2 Server"
       schema:
         openAPIV3Schema:
           type: object
@@ -130,3 +132,91 @@ spec:
         type: string
         description: The protocol of the server
         jsonPath: .spec.proxyProtocol
+    - name: v1beta2
+      served: true
+      storage: true
+      schema:
+        openAPIV3Schema:
+          type: object
+          required: [spec]
+          properties:
+            spec:
+              type: object
+              required:
+                - port
+              oneOf:
+                - required: [podSelector]
+                - required: [externalWorkloadSelector]
+              properties:
+                podSelector:
+                  type: object
+                  description: >-
+                    Selects pods in the same namespace.
+
+                    The result of matchLabels and matchExpressions are ANDed.
+                    Selects all if empty.
+                  properties:
+                    matchLabels:
+                      type: object
+                      x-kubernetes-preserve-unknown-fields: true
+                    matchExpressions:
+                      type: array
+                      items:
+                        type: object
+                        required: [key, operator]
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                            enum: [In, NotIn, Exists, DoesNotExist]
+                          values:
+                            type: array
+                            items:
+                              type: string
+                externalWorkloadSelector:
+                  type: object
+                  description: >-
+                    Selects ExternalWorkloads in the same namespace.
+
+                    The result of matchLabels and matchExpressions are ANDed.
+                    Selects all if empty.
+                  properties:
+                    matchLabels:
+                      type: object
+                      x-kubernetes-preserve-unknown-fields: true
+                    matchExpressions:
+                      type: array
+                      items:
+                        type: object
+                        required: [key, operator]
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                            enum: [In, NotIn, Exists, DoesNotExist]
+                          values:
+                            type: array
+                            items:
+                              type: string
+                port:
+                  description: >-
+                    A port name or number. Must exist in a pod spec.
+                  x-kubernetes-int-or-string: true
+                proxyProtocol:
+                  description: >-
+                    Configures protocol discovery for inbound connections.
+
+                    Supersedes the `config.linkerd.io/opaque-ports` annotation.
+                  type: string
+                  default: unknown
+      additionalPrinterColumns:
+      - name: Port
+        type: string
+        description: The port the server is listening on
+        jsonPath: .spec.port
+      - name: Protocol
+        type: string
+        description: The protocol of the server
+        jsonPath: .spec.proxyProtocol
diff --git a/cli/cmd/endpoints.go b/cli/cmd/endpoints.go
index 1e835d918..a25238b00 100644
--- a/cli/cmd/endpoints.go
+++ b/cli/cmd/endpoints.go
@@ -37,6 +37,8 @@ type (
 		name    string
 		address string
 		ip      string
+		weight  uint32
+		labels  map[string]string
 	}
 )
 
@@ -200,6 +202,8 @@ func requestEndpointsFromAPI(client destinationPb.DestinationClient, authorities
 					name:    labels["pod"],
 					address: tcpAddr.String(),
 					ip:      getIP(tcpAddr),
+					weight:  addr.GetWeight(),
+					labels:  addr.GetMetricLabels(),
 				})
 			}
 		}
@@ -230,6 +234,9 @@ type rowEndpoint struct {
 	Port      uint32 `json:"port"`
 	Pod       string `json:"pod"`
 	Service   string `json:"service"`
+	Weight    uint32 `json:"weight"`
+
+	Labels map[string]string `json:"labels"`
 }
 
 func writeEndpointsToBuffer(endpoints endpointsInfo, w *tabwriter.Writer, options *endpointsOptions) {
@@ -255,6 +262,8 @@ func writeEndpointsToBuffer(endpoints endpointsInfo, w *tabwriter.Writer, option
 					Port:      port,
 					Pod:       name,
 					Service:   serviceID,
+					Weight:    pod.weight,
+					Labels:    pod.labels,
 				}
 
 				endpointsTables[namespace] = append(endpointsTables[namespace], row)
diff --git a/controller/gen/apis/externalworkload/register.go b/controller/gen/apis/externalworkload/register.go
new file mode 100644
index 000000000..514e995e7
--- /dev/null
+++ b/controller/gen/apis/externalworkload/register.go
@@ -0,0 +1,4 @@
+package externalworkload
+
+// GroupName identifies the API Group name for an ExternalWorkload
+const GroupName = "workload.linkerd.io"
diff --git a/controller/gen/apis/externalworkload/v1alpha1/doc.go b/controller/gen/apis/externalworkload/v1alpha1/doc.go
new file mode 100644
index 000000000..4601689c3
--- /dev/null
+++ b/controller/gen/apis/externalworkload/v1alpha1/doc.go
@@ -0,0 +1,3 @@
+// +k8s:deepcopy-gen=package
+
+package v1alpha1
diff --git a/controller/gen/apis/externalworkload/v1alpha1/register.go b/controller/gen/apis/externalworkload/v1alpha1/register.go
new file mode 100644
index 000000000..d47f6e9a3
--- /dev/null
+++ b/controller/gen/apis/externalworkload/v1alpha1/register.go
@@ -0,0 +1,49 @@
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/linkerd/linkerd2/controller/gen/apis/externalworkload"
+)
+
+var (
+	// SchemeGroupVersion is the identifier for the API which includes the name
+	// of the group and the version of the API.
+	SchemeGroupVersion = schema.GroupVersion{
+		Group:   externalworkload.GroupName,
+		Version: "v1alpha1",
+	}
+
+	// SchemeBuilder collects functions that add things to a scheme. It's to
+	// allow code to compile without explicitly referencing generated types.
+	// You should declare one in each package that will have generated deep
+	// copy or conversion functions.
+	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+
+	// AddToScheme applies all the stored functions to the scheme. A non-nil error
+	// indicates that one function failed and the attempt was abandoned.
+	AddToScheme = SchemeBuilder.AddToScheme
+)
+
+// Kind takes an unqualified kind and returns back a Group qualified GroupKind
+func Kind(kind string) schema.GroupKind {
+	return SchemeGroupVersion.WithKind(kind).GroupKind()
+}
+
+// Resource takes an unqualified resource and returns a Group qualified
+// GroupResource
+func Resource(resource string) schema.GroupResource {
+	return SchemeGroupVersion.WithResource(resource).GroupResource()
+}
+
+// Adds the list of known types to Scheme.
+func addKnownTypes(scheme *runtime.Scheme) error {
+	scheme.AddKnownTypes(SchemeGroupVersion,
+		&ExternalWorkload{},
+		&ExternalWorkloadList{},
+	)
+	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
+	return nil
+}
diff --git a/controller/gen/apis/externalworkload/v1alpha1/types.go b/controller/gen/apis/externalworkload/v1alpha1/types.go
new file mode 100644
index 000000000..fae321dcc
--- /dev/null
+++ b/controller/gen/apis/externalworkload/v1alpha1/types.go
@@ -0,0 +1,136 @@
+package v1alpha1
+
+import (
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// +genclient
+// +genclient:noStatus
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +groupName=workload.linkerd.io
+
+// ExternalWorkload describes a single workload (i.e. a deployable unit,
+// conceptually similar to a Kubernetes Pod) that is running outside of a
+// Kubernetes cluster. An ExternalWorkload should be enrolled in the mesh and
+// typically represents a virtual machine.
+type ExternalWorkload struct {
+	// TypeMeta is the metadata for the resource, like kind and apiversion
+	metav1.TypeMeta `json:",inline"`
+	// ObjectMeta contains the metadata for the particular object, including
+	// things like...
+	//  - name
+	//  - namespace
+	//  - self link
+	//  - labels
+	//  - ... etc ...
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	// Spec defines the desired state of an external workload instance
+	Spec ExternalWorkloadSpec `json:"spec"`
+
+	// Status defines the current state of an external workload instance
+	Status ExternalWorkloadStatus `json:"status,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// ExternalWorkloadList contains a list of ExternalWorkload resources.
+type ExternalWorkloadList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata"`
+
+	Items []ExternalWorkload `json:"items"`
+}
+
+// ExternalWorkloadSpec represents the desired state of an external workload
+type ExternalWorkloadSpec struct {
+	// MeshTls describes TLS settings associated with an external workload
+	//
+	// +optional
+	MeshTls MeshTls `json:"meshTls,omitempty"`
+	// Ports describes a set of ports exposed by the workload
+	Ports []PortSpec `json:"ports"`
+	// List of IP addresses that can be used to send traffic to an external
+	// workload
+	WorkloadIPs []WorkloadIP `json:"workloadIPs"`
+}
+
+// MeshTls describes TLS settings associated with an external workload
+type MeshTls struct {
+	// Identity associated with the workload. Used by peers to perform
+	// verification in the mTLS handshake
+	Identity string `json:"identity,omitempty"`
+	// ServerName is the DNS formatted name associated with the workload. Used
+	// to terminate TLS using the SNI extension.
+	ServerName string `json:"serverName,omitempty"`
+}
+
+// PortSpec represents a network port in a single workload.
+type PortSpec struct {
+	// If specified, must be an IANA_SVC_NAME and unique within the exposed
+	// ports set. Each named port must have a unique name. The name may be
+	// referred to by services
+	// +optional
+	Name string `json:"name,omitempty"`
+	// Number of port exposed on the workload's IP address.
+	// Must be a valid port number, i.e. 0 < x < 65536.
+	Port int32 `json:"port"`
+	// Protocol defines network protocols supported. One of UDP, TCP, or SCTP.
+	// Should coincide with Service selecting the workload.
+	// Defaults to "TCP" if unspecified.
+	// +optional
+	// +default="TCP"
+	Protocol v1.Protocol `json:"protocol,omitempty"`
+}
+
+// WorkloadIPs contains a list of IP addresses exposed by an ExternalWorkload
+type WorkloadIP struct {
+	Ip string `json:"ip"`
+}
+
+// WorkloadStatus holds information about the status of an external workload.
+// The status describes the state of the workload.
+type ExternalWorkloadStatus struct {
+	// Current service state of an ExternalWorkload
+	// +optional
+	Conditions []WorkloadCondition `json:"conditions,omitempty"`
+}
+
+// WorkloadCondition represents the service state of an ExternalWorkload
+type WorkloadCondition struct {
+	// Type of the condition
+	// see: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#pod-conditions
+	Type WorkloadConditionType `json:"type"`
+	// Status of the condition.
+	// Can be True, False, Unknown
+	Status WorkloadConditionStatus `json:"status"`
+	// Last time an ExternalWorkload was probed for a condition.
+	// +optional
+	LastProbeTime metav1.Time `json:"lastProbeTime,omitempty"`
+	// Last time a condition transitioned from one status to another.
+	LastTransitionTime metav1.Time `json:"lastTransitionTime"`
+	// Unique one word reason in CamelCase that describes the reason for a
+	// transition.
+	Reason string `json:"reason"`
+	// Human readable message that describes details about last transition.
+	Message string `json:"message"`
+}
+
+// WorkloadConditionType is a value for the type of a condition in an
+// ExternalWorkload's status
+type WorkloadConditionType string
+
+const (
+	// Ready to serve traffic
+	WorkloadReady WorkloadConditionType = "Ready"
+)
+
+// WorkloadConditionStatus
+type WorkloadConditionStatus string
+
+const (
+	ConditionTrue    WorkloadConditionStatus = "True"
+	ConditionFalse   WorkloadConditionStatus = "False"
+	ConditionUnknown WorkloadConditionStatus = "Unknown"
+)
diff --git a/controller/gen/apis/externalworkload/v1alpha1/zz_generated.deepcopy.go b/controller/gen/apis/externalworkload/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 000000000..0c312b71c
--- /dev/null
+++ b/controller/gen/apis/externalworkload/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,203 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExternalWorkload) DeepCopyInto(out *ExternalWorkload) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalWorkload.
+func (in *ExternalWorkload) DeepCopy() *ExternalWorkload {
+	if in == nil {
+		return nil
+	}
+	out := new(ExternalWorkload)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ExternalWorkload) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExternalWorkloadList) DeepCopyInto(out *ExternalWorkloadList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]ExternalWorkload, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalWorkloadList.
+func (in *ExternalWorkloadList) DeepCopy() *ExternalWorkloadList {
+	if in == nil {
+		return nil
+	}
+	out := new(ExternalWorkloadList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ExternalWorkloadList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExternalWorkloadSpec) DeepCopyInto(out *ExternalWorkloadSpec) {
+	*out = *in
+	out.MeshTls = in.MeshTls
+	if in.Ports != nil {
+		in, out := &in.Ports, &out.Ports
+		*out = make([]PortSpec, len(*in))
+		copy(*out, *in)
+	}
+	if in.WorkloadIPs != nil {
+		in, out := &in.WorkloadIPs, &out.WorkloadIPs
+		*out = make([]WorkloadIP, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalWorkloadSpec.
+func (in *ExternalWorkloadSpec) DeepCopy() *ExternalWorkloadSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ExternalWorkloadSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExternalWorkloadStatus) DeepCopyInto(out *ExternalWorkloadStatus) {
+	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]WorkloadCondition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalWorkloadStatus.
+func (in *ExternalWorkloadStatus) DeepCopy() *ExternalWorkloadStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(ExternalWorkloadStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MeshTls) DeepCopyInto(out *MeshTls) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MeshTls.
+func (in *MeshTls) DeepCopy() *MeshTls {
+	if in == nil {
+		return nil
+	}
+	out := new(MeshTls)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PortSpec) DeepCopyInto(out *PortSpec) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PortSpec.
+func (in *PortSpec) DeepCopy() *PortSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(PortSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkloadCondition) DeepCopyInto(out *WorkloadCondition) {
+	*out = *in
+	in.LastProbeTime.DeepCopyInto(&out.LastProbeTime)
+	in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadCondition.
+func (in *WorkloadCondition) DeepCopy() *WorkloadCondition {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkloadCondition)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkloadIP) DeepCopyInto(out *WorkloadIP) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadIP.
+func (in *WorkloadIP) DeepCopy() *WorkloadIP {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkloadIP)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/controller/gen/client/clientset/versioned/clientset.go b/controller/gen/client/clientset/versioned/clientset.go
index 3d669131b..ae1460eb1 100644
--- a/controller/gen/client/clientset/versioned/clientset.go
+++ b/controller/gen/client/clientset/versioned/clientset.go
@@ -22,6 +22,7 @@ import (
 	"fmt"
 	"net/http"
 
+	externalworkloadv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1"
 	linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/link/v1alpha1"
 	policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1alpha1"
 	policyv1beta3 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1beta3"
@@ -35,6 +36,7 @@ import (
 
 type Interface interface {
 	Discovery() discovery.DiscoveryInterface
+	ExternalworkloadV1alpha1() externalworkloadv1alpha1.ExternalworkloadV1alpha1Interface
 	LinkV1alpha1() linkv1alpha1.LinkV1alpha1Interface
 	PolicyV1alpha1() policyv1alpha1.PolicyV1alpha1Interface
 	PolicyV1beta3() policyv1beta3.PolicyV1beta3Interface
@@ -46,6 +48,7 @@ type Interface interface {
 // Clientset contains the clients for groups.
 type Clientset struct {
 	*discovery.DiscoveryClient
+	externalworkloadV1alpha1   *externalworkloadv1alpha1.ExternalworkloadV1alpha1Client
 	linkV1alpha1               *linkv1alpha1.LinkV1alpha1Client
 	policyV1alpha1             *policyv1alpha1.PolicyV1alpha1Client
 	policyV1beta3              *policyv1beta3.PolicyV1beta3Client
@@ -54,6 +57,11 @@ type Clientset struct {
 	linkerdV1alpha2            *linkerdv1alpha2.LinkerdV1alpha2Client
 }
 
+// ExternalworkloadV1alpha1 retrieves the ExternalworkloadV1alpha1Client
+func (c *Clientset) ExternalworkloadV1alpha1() externalworkloadv1alpha1.ExternalworkloadV1alpha1Interface {
+	return c.externalworkloadV1alpha1
+}
+
 // LinkV1alpha1 retrieves the LinkV1alpha1Client
 func (c *Clientset) LinkV1alpha1() linkv1alpha1.LinkV1alpha1Interface {
 	return c.linkV1alpha1
@@ -128,6 +136,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset,
 
 	var cs Clientset
 	var err error
+	cs.externalworkloadV1alpha1, err = externalworkloadv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient)
+	if err != nil {
+		return nil, err
+	}
 	cs.linkV1alpha1, err = linkv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient)
 	if err != nil {
 		return nil, err
@@ -173,6 +185,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset {
 // New creates a new Clientset for the given RESTClient.
 func New(c rest.Interface) *Clientset {
 	var cs Clientset
+	cs.externalworkloadV1alpha1 = externalworkloadv1alpha1.New(c)
 	cs.linkV1alpha1 = linkv1alpha1.New(c)
 	cs.policyV1alpha1 = policyv1alpha1.New(c)
 	cs.policyV1beta3 = policyv1beta3.New(c)
diff --git a/controller/gen/client/clientset/versioned/fake/clientset_generated.go b/controller/gen/client/clientset/versioned/fake/clientset_generated.go
index 12118b595..3a7b8d51f 100644
--- a/controller/gen/client/clientset/versioned/fake/clientset_generated.go
+++ b/controller/gen/client/clientset/versioned/fake/clientset_generated.go
@@ -20,6 +20,8 @@ package fake
 
 import (
 	clientset "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	externalworkloadv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1"
+	fakeexternalworkloadv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake"
 	linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/link/v1alpha1"
 	fakelinkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/link/v1alpha1/fake"
 	policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1alpha1"
@@ -89,6 +91,11 @@ var (
 	_ testing.FakeClient  = &Clientset{}
 )
 
+// ExternalworkloadV1alpha1 retrieves the ExternalworkloadV1alpha1Client
+func (c *Clientset) ExternalworkloadV1alpha1() externalworkloadv1alpha1.ExternalworkloadV1alpha1Interface {
+	return &fakeexternalworkloadv1alpha1.FakeExternalworkloadV1alpha1{Fake: &c.Fake}
+}
+
 // LinkV1alpha1 retrieves the LinkV1alpha1Client
 func (c *Clientset) LinkV1alpha1() linkv1alpha1.LinkV1alpha1Interface {
 	return &fakelinkv1alpha1.FakeLinkV1alpha1{Fake: &c.Fake}
diff --git a/controller/gen/client/clientset/versioned/fake/register.go b/controller/gen/client/clientset/versioned/fake/register.go
index 3690f3b14..4271e8af8 100644
--- a/controller/gen/client/clientset/versioned/fake/register.go
+++ b/controller/gen/client/clientset/versioned/fake/register.go
@@ -19,6 +19,7 @@ limitations under the License.
 package fake
 
 import (
+	externalworkloadv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
 	linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha1"
 	policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1alpha1"
 	policyv1beta3 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1beta3"
@@ -36,6 +37,7 @@ var scheme = runtime.NewScheme()
 var codecs = serializer.NewCodecFactory(scheme)
 
 var localSchemeBuilder = runtime.SchemeBuilder{
+	externalworkloadv1alpha1.AddToScheme,
 	linkv1alpha1.AddToScheme,
 	policyv1alpha1.AddToScheme,
 	policyv1beta3.AddToScheme,
diff --git a/controller/gen/client/clientset/versioned/scheme/register.go b/controller/gen/client/clientset/versioned/scheme/register.go
index f2f32b645..e266dcec9 100644
--- a/controller/gen/client/clientset/versioned/scheme/register.go
+++ b/controller/gen/client/clientset/versioned/scheme/register.go
@@ -19,6 +19,7 @@ limitations under the License.
 package scheme
 
 import (
+	externalworkloadv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
 	linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha1"
 	policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1alpha1"
 	policyv1beta3 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1beta3"
@@ -36,6 +37,7 @@ var Scheme = runtime.NewScheme()
 var Codecs = serializer.NewCodecFactory(Scheme)
 var ParameterCodec = runtime.NewParameterCodec(Scheme)
 var localSchemeBuilder = runtime.SchemeBuilder{
+	externalworkloadv1alpha1.AddToScheme,
 	linkv1alpha1.AddToScheme,
 	policyv1alpha1.AddToScheme,
 	policyv1beta3.AddToScheme,
diff --git a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/doc.go b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/doc.go
new file mode 100644
index 000000000..df51baa4d
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated typed clients.
+package v1alpha1
diff --git a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload.go b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload.go
new file mode 100644
index 000000000..ac57766a6
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload.go
@@ -0,0 +1,178 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	"time"
+
+	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	scheme "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// ExternalWorkloadsGetter has a method to return a ExternalWorkloadInterface.
+// A group's client should implement this interface.
+type ExternalWorkloadsGetter interface {
+	ExternalWorkloads(namespace string) ExternalWorkloadInterface
+}
+
+// ExternalWorkloadInterface has methods to work with ExternalWorkload resources.
+type ExternalWorkloadInterface interface {
+	Create(ctx context.Context, externalWorkload *v1alpha1.ExternalWorkload, opts v1.CreateOptions) (*v1alpha1.ExternalWorkload, error)
+	Update(ctx context.Context, externalWorkload *v1alpha1.ExternalWorkload, opts v1.UpdateOptions) (*v1alpha1.ExternalWorkload, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.ExternalWorkload, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.ExternalWorkloadList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ExternalWorkload, err error)
+	ExternalWorkloadExpansion
+}
+
+// externalWorkloads implements ExternalWorkloadInterface
+type externalWorkloads struct {
+	client rest.Interface
+	ns     string
+}
+
+// newExternalWorkloads returns a ExternalWorkloads
+func newExternalWorkloads(c *ExternalworkloadV1alpha1Client, namespace string) *externalWorkloads {
+	return &externalWorkloads{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the externalWorkload, and returns the corresponding externalWorkload object, and an error if there is any.
+func (c *externalWorkloads) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ExternalWorkload, err error) {
+	result = &v1alpha1.ExternalWorkload{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("externalworkloads").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of ExternalWorkloads that match those selectors.
+func (c *externalWorkloads) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ExternalWorkloadList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1alpha1.ExternalWorkloadList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("externalworkloads").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested externalWorkloads.
+func (c *externalWorkloads) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("externalworkloads").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a externalWorkload and creates it.  Returns the server's representation of the externalWorkload, and an error, if there is any.
+func (c *externalWorkloads) Create(ctx context.Context, externalWorkload *v1alpha1.ExternalWorkload, opts v1.CreateOptions) (result *v1alpha1.ExternalWorkload, err error) {
+	result = &v1alpha1.ExternalWorkload{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("externalworkloads").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(externalWorkload).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a externalWorkload and updates it. Returns the server's representation of the externalWorkload, and an error, if there is any.
+func (c *externalWorkloads) Update(ctx context.Context, externalWorkload *v1alpha1.ExternalWorkload, opts v1.UpdateOptions) (result *v1alpha1.ExternalWorkload, err error) {
+	result = &v1alpha1.ExternalWorkload{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("externalworkloads").
+		Name(externalWorkload.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(externalWorkload).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the externalWorkload and deletes it. Returns an error if one occurs.
+func (c *externalWorkloads) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("externalworkloads").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *externalWorkloads) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("externalworkloads").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched externalWorkload.
+func (c *externalWorkloads) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ExternalWorkload, err error) {
+	result = &v1alpha1.ExternalWorkload{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("externalworkloads").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload_client.go b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload_client.go
new file mode 100644
index 000000000..0a58f2919
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload_client.go
@@ -0,0 +1,107 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"net/http"
+
+	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	"github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme"
+	rest "k8s.io/client-go/rest"
+)
+
+type ExternalworkloadV1alpha1Interface interface {
+	RESTClient() rest.Interface
+	ExternalWorkloadsGetter
+}
+
+// ExternalworkloadV1alpha1Client is used to interact with features provided by the externalworkload group.
+type ExternalworkloadV1alpha1Client struct {
+	restClient rest.Interface
+}
+
+func (c *ExternalworkloadV1alpha1Client) ExternalWorkloads(namespace string) ExternalWorkloadInterface {
+	return newExternalWorkloads(c, namespace)
+}
+
+// NewForConfig creates a new ExternalworkloadV1alpha1Client for the given config.
+// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
+// where httpClient was generated with rest.HTTPClientFor(c).
+func NewForConfig(c *rest.Config) (*ExternalworkloadV1alpha1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	httpClient, err := rest.HTTPClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return NewForConfigAndClient(&config, httpClient)
+}
+
+// NewForConfigAndClient creates a new ExternalworkloadV1alpha1Client for the given config and http client.
+// Note the http client provided takes precedence over the configured transport values.
+func NewForConfigAndClient(c *rest.Config, h *http.Client) (*ExternalworkloadV1alpha1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientForConfigAndClient(&config, h)
+	if err != nil {
+		return nil, err
+	}
+	return &ExternalworkloadV1alpha1Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new ExternalworkloadV1alpha1Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *ExternalworkloadV1alpha1Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new ExternalworkloadV1alpha1Client for the given RESTClient.
+func New(c rest.Interface) *ExternalworkloadV1alpha1Client {
+	return &ExternalworkloadV1alpha1Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) error {
+	gv := v1alpha1.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *ExternalworkloadV1alpha1Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/doc.go b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/doc.go
new file mode 100644
index 000000000..16f443990
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// Package fake has the automatically generated clients.
+package fake
diff --git a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/fake_externalworkload.go b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/fake_externalworkload.go
new file mode 100644
index 000000000..7d5212df5
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/fake_externalworkload.go
@@ -0,0 +1,129 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeExternalWorkloads implements ExternalWorkloadInterface
+type FakeExternalWorkloads struct {
+	Fake *FakeExternalworkloadV1alpha1
+	ns   string
+}
+
+var externalworkloadsResource = v1alpha1.SchemeGroupVersion.WithResource("externalworkloads")
+
+var externalworkloadsKind = v1alpha1.SchemeGroupVersion.WithKind("ExternalWorkload")
+
+// Get takes name of the externalWorkload, and returns the corresponding externalWorkload object, and an error if there is any.
+func (c *FakeExternalWorkloads) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ExternalWorkload, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(externalworkloadsResource, c.ns, name), &v1alpha1.ExternalWorkload{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.ExternalWorkload), err
+}
+
+// List takes label and field selectors, and returns the list of ExternalWorkloads that match those selectors.
+func (c *FakeExternalWorkloads) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ExternalWorkloadList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(externalworkloadsResource, externalworkloadsKind, c.ns, opts), &v1alpha1.ExternalWorkloadList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1alpha1.ExternalWorkloadList{ListMeta: obj.(*v1alpha1.ExternalWorkloadList).ListMeta}
+	for _, item := range obj.(*v1alpha1.ExternalWorkloadList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested externalWorkloads.
+func (c *FakeExternalWorkloads) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(externalworkloadsResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a externalWorkload and creates it.  Returns the server's representation of the externalWorkload, and an error, if there is any.
+func (c *FakeExternalWorkloads) Create(ctx context.Context, externalWorkload *v1alpha1.ExternalWorkload, opts v1.CreateOptions) (result *v1alpha1.ExternalWorkload, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(externalworkloadsResource, c.ns, externalWorkload), &v1alpha1.ExternalWorkload{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.ExternalWorkload), err
+}
+
+// Update takes the representation of a externalWorkload and updates it. Returns the server's representation of the externalWorkload, and an error, if there is any.
+func (c *FakeExternalWorkloads) Update(ctx context.Context, externalWorkload *v1alpha1.ExternalWorkload, opts v1.UpdateOptions) (result *v1alpha1.ExternalWorkload, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(externalworkloadsResource, c.ns, externalWorkload), &v1alpha1.ExternalWorkload{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.ExternalWorkload), err
+}
+
+// Delete takes name of the externalWorkload and deletes it. Returns an error if one occurs.
+func (c *FakeExternalWorkloads) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteActionWithOptions(externalworkloadsResource, c.ns, name, opts), &v1alpha1.ExternalWorkload{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeExternalWorkloads) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(externalworkloadsResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1alpha1.ExternalWorkloadList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched externalWorkload.
+func (c *FakeExternalWorkloads) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ExternalWorkload, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(externalworkloadsResource, c.ns, name, pt, data, subresources...), &v1alpha1.ExternalWorkload{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.ExternalWorkload), err
+}
diff --git a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/fake_externalworkload_client.go b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/fake_externalworkload_client.go
new file mode 100644
index 000000000..b0c3ba2f9
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/fake_externalworkload_client.go
@@ -0,0 +1,40 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1"
+	rest "k8s.io/client-go/rest"
+	testing "k8s.io/client-go/testing"
+)
+
+type FakeExternalworkloadV1alpha1 struct {
+	*testing.Fake
+}
+
+func (c *FakeExternalworkloadV1alpha1) ExternalWorkloads(namespace string) v1alpha1.ExternalWorkloadInterface {
+	return &FakeExternalWorkloads{c, namespace}
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *FakeExternalworkloadV1alpha1) RESTClient() rest.Interface {
+	var ret *rest.RESTClient
+	return ret
+}
diff --git a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/generated_expansion.go b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/generated_expansion.go
new file mode 100644
index 000000000..cbd85d00b
--- /dev/null
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/generated_expansion.go
@@ -0,0 +1,21 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+type ExternalWorkloadExpansion interface{}
diff --git a/controller/gen/client/informers/externalversions/externalworkload/interface.go b/controller/gen/client/informers/externalversions/externalworkload/interface.go
new file mode 100644
index 000000000..b4a55f4e7
--- /dev/null
+++ b/controller/gen/client/informers/externalversions/externalworkload/interface.go
@@ -0,0 +1,46 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package externalworkload
+
+import (
+	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/externalworkload/v1alpha1"
+	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to each of this group's versions.
+type Interface interface {
+	// V1alpha1 provides access to shared informers for resources in V1alpha1.
+	V1alpha1() v1alpha1.Interface
+}
+
+type group struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// V1alpha1 returns a new v1alpha1.Interface.
+func (g *group) V1alpha1() v1alpha1.Interface {
+	return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions)
+}
diff --git a/controller/gen/client/informers/externalversions/externalworkload/v1alpha1/externalworkload.go b/controller/gen/client/informers/externalversions/externalworkload/v1alpha1/externalworkload.go
new file mode 100644
index 000000000..3e748b2ca
--- /dev/null
+++ b/controller/gen/client/informers/externalversions/externalworkload/v1alpha1/externalworkload.go
@@ -0,0 +1,90 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	time "time"
+
+	externalworkloadv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	versioned "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/listers/externalworkload/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// ExternalWorkloadInformer provides access to a shared informer and lister for
+// ExternalWorkloads.
+type ExternalWorkloadInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1alpha1.ExternalWorkloadLister
+}
+
+type externalWorkloadInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewExternalWorkloadInformer constructs a new informer for ExternalWorkload type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewExternalWorkloadInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredExternalWorkloadInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredExternalWorkloadInformer constructs a new informer for ExternalWorkload type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredExternalWorkloadInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.ExternalworkloadV1alpha1().ExternalWorkloads(namespace).List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.ExternalworkloadV1alpha1().ExternalWorkloads(namespace).Watch(context.TODO(), options)
+			},
+		},
+		&externalworkloadv1alpha1.ExternalWorkload{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *externalWorkloadInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredExternalWorkloadInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *externalWorkloadInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&externalworkloadv1alpha1.ExternalWorkload{}, f.defaultInformer)
+}
+
+func (f *externalWorkloadInformer) Lister() v1alpha1.ExternalWorkloadLister {
+	return v1alpha1.NewExternalWorkloadLister(f.Informer().GetIndexer())
+}
diff --git a/controller/gen/client/informers/externalversions/externalworkload/v1alpha1/interface.go b/controller/gen/client/informers/externalversions/externalworkload/v1alpha1/interface.go
new file mode 100644
index 000000000..1866f95de
--- /dev/null
+++ b/controller/gen/client/informers/externalversions/externalworkload/v1alpha1/interface.go
@@ -0,0 +1,45 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to all the informers in this group version.
+type Interface interface {
+	// ExternalWorkloads returns a ExternalWorkloadInformer.
+	ExternalWorkloads() ExternalWorkloadInformer
+}
+
+type version struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// ExternalWorkloads returns a ExternalWorkloadInformer.
+func (v *version) ExternalWorkloads() ExternalWorkloadInformer {
+	return &externalWorkloadInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
diff --git a/controller/gen/client/informers/externalversions/factory.go b/controller/gen/client/informers/externalversions/factory.go
index 171f32bfb..2cdf7b777 100644
--- a/controller/gen/client/informers/externalversions/factory.go
+++ b/controller/gen/client/informers/externalversions/factory.go
@@ -24,6 +24,7 @@ import (
 	time "time"
 
 	versioned "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	externalworkload "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/externalworkload"
 	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
 	link "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/link"
 	policy "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/policy"
@@ -46,6 +47,7 @@ type sharedInformerFactory struct {
 	lock             sync.Mutex
 	defaultResync    time.Duration
 	customResync     map[reflect.Type]time.Duration
+	transform        cache.TransformFunc
 
 	informers map[reflect.Type]cache.SharedIndexInformer
 	// startedInformers is used for tracking which informers have been started.
@@ -84,6 +86,14 @@ func WithNamespace(namespace string) SharedInformerOption {
 	}
 }
 
+// WithTransform sets a transform on all informers.
+func WithTransform(transform cache.TransformFunc) SharedInformerOption {
+	return func(factory *sharedInformerFactory) *sharedInformerFactory {
+		factory.transform = transform
+		return factory
+	}
+}
+
 // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces.
 func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory {
 	return NewSharedInformerFactoryWithOptions(client, defaultResync)
@@ -188,6 +198,7 @@ func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internal
 	}
 
 	informer = newFunc(f.client, resyncPeriod)
+	informer.SetTransform(f.transform)
 	f.informers[informerType] = informer
 
 	return informer
@@ -247,6 +258,7 @@ type SharedInformerFactory interface {
 	// client.
 	InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer
 
+	Externalworkload() externalworkload.Interface
 	Link() link.Interface
 	Policy() policy.Interface
 	Server() server.Interface
@@ -254,6 +266,10 @@ type SharedInformerFactory interface {
 	Linkerd() serviceprofile.Interface
 }
 
+func (f *sharedInformerFactory) Externalworkload() externalworkload.Interface {
+	return externalworkload.New(f, f.namespace, f.tweakListOptions)
+}
+
 func (f *sharedInformerFactory) Link() link.Interface {
 	return link.New(f, f.namespace, f.tweakListOptions)
 }
diff --git a/controller/gen/client/informers/externalversions/generic.go b/controller/gen/client/informers/externalversions/generic.go
index df19bbe93..cb6821e16 100644
--- a/controller/gen/client/informers/externalversions/generic.go
+++ b/controller/gen/client/informers/externalversions/generic.go
@@ -21,7 +21,8 @@ package externalversions
 import (
 	"fmt"
 
-	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha1"
+	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha1"
 	policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1alpha1"
 	v1beta3 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1beta3"
 	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
@@ -57,8 +58,12 @@ func (f *genericInformer) Lister() cache.GenericLister {
 // TODO extend this to unknown resources with a client pool
 func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
 	switch resource {
-	// Group=link, Version=v1alpha1
-	case v1alpha1.SchemeGroupVersion.WithResource("links"):
+	// Group=externalworkload, Version=v1alpha1
+	case v1alpha1.SchemeGroupVersion.WithResource("externalworkloads"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Externalworkload().V1alpha1().ExternalWorkloads().Informer()}, nil
+
+		// Group=link, Version=v1alpha1
+	case linkv1alpha1.SchemeGroupVersion.WithResource("links"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Link().V1alpha1().Links().Informer()}, nil
 
 		// Group=linkerd.io, Version=v1alpha2
diff --git a/controller/gen/client/listers/externalworkload/v1alpha1/expansion_generated.go b/controller/gen/client/listers/externalworkload/v1alpha1/expansion_generated.go
new file mode 100644
index 000000000..5c77b2b63
--- /dev/null
+++ b/controller/gen/client/listers/externalworkload/v1alpha1/expansion_generated.go
@@ -0,0 +1,27 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// ExternalWorkloadListerExpansion allows custom methods to be added to
+// ExternalWorkloadLister.
+type ExternalWorkloadListerExpansion interface{}
+
+// ExternalWorkloadNamespaceListerExpansion allows custom methods to be added to
+// ExternalWorkloadNamespaceLister.
+type ExternalWorkloadNamespaceListerExpansion interface{}
diff --git a/controller/gen/client/listers/externalworkload/v1alpha1/externalworkload.go b/controller/gen/client/listers/externalworkload/v1alpha1/externalworkload.go
new file mode 100644
index 000000000..7b39381af
--- /dev/null
+++ b/controller/gen/client/listers/externalworkload/v1alpha1/externalworkload.go
@@ -0,0 +1,99 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// ExternalWorkloadLister helps list ExternalWorkloads.
+// All objects returned here must be treated as read-only.
+type ExternalWorkloadLister interface {
+	// List lists all ExternalWorkloads in the indexer.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1alpha1.ExternalWorkload, err error)
+	// ExternalWorkloads returns an object that can list and get ExternalWorkloads.
+	ExternalWorkloads(namespace string) ExternalWorkloadNamespaceLister
+	ExternalWorkloadListerExpansion
+}
+
+// externalWorkloadLister implements the ExternalWorkloadLister interface.
+type externalWorkloadLister struct {
+	indexer cache.Indexer
+}
+
+// NewExternalWorkloadLister returns a new ExternalWorkloadLister.
+func NewExternalWorkloadLister(indexer cache.Indexer) ExternalWorkloadLister {
+	return &externalWorkloadLister{indexer: indexer}
+}
+
+// List lists all ExternalWorkloads in the indexer.
+func (s *externalWorkloadLister) List(selector labels.Selector) (ret []*v1alpha1.ExternalWorkload, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.ExternalWorkload))
+	})
+	return ret, err
+}
+
+// ExternalWorkloads returns an object that can list and get ExternalWorkloads.
+func (s *externalWorkloadLister) ExternalWorkloads(namespace string) ExternalWorkloadNamespaceLister {
+	return externalWorkloadNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// ExternalWorkloadNamespaceLister helps list and get ExternalWorkloads.
+// All objects returned here must be treated as read-only.
+type ExternalWorkloadNamespaceLister interface {
+	// List lists all ExternalWorkloads in the indexer for a given namespace.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1alpha1.ExternalWorkload, err error)
+	// Get retrieves the ExternalWorkload from the indexer for a given namespace and name.
+	// Objects returned here must be treated as read-only.
+	Get(name string) (*v1alpha1.ExternalWorkload, error)
+	ExternalWorkloadNamespaceListerExpansion
+}
+
+// externalWorkloadNamespaceLister implements the ExternalWorkloadNamespaceLister
+// interface.
+type externalWorkloadNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all ExternalWorkloads in the indexer for a given namespace.
+func (s externalWorkloadNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.ExternalWorkload, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.ExternalWorkload))
+	})
+	return ret, err
+}
+
+// Get retrieves the ExternalWorkload from the indexer for a given namespace and name.
+func (s externalWorkloadNamespaceLister) Get(name string) (*v1alpha1.ExternalWorkload, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1alpha1.Resource("externalworkload"), name)
+	}
+	return obj.(*v1alpha1.ExternalWorkload), nil
+}
diff --git a/controller/k8s/api.go b/controller/k8s/api.go
index 728c3959f..7003c6d8e 100644
--- a/controller/k8s/api.go
+++ b/controller/k8s/api.go
@@ -11,6 +11,7 @@ import (
 	spv1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
 	l5dcrdclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
 	l5dcrdinformer "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions"
+	ewinformers "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/externalworkload/v1alpha1"
 	srvinformers "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/server/v1beta1"
 	spinformers "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/serviceprofile/v1alpha2"
 	"github.com/linkerd/linkerd2/pkg/k8s"
@@ -49,6 +50,7 @@ type API struct {
 	ds       appv1informers.DaemonSetInformer
 	endpoint coreinformers.EndpointsInformer
 	es       discoveryinformers.EndpointSliceInformer
+	ew       ewinformers.ExternalWorkloadInformer
 	job      batchv1informers.JobInformer
 	mwc      arinformers.MutatingWebhookConfigurationInformer
 	ns       coreinformers.NamespaceInformer
@@ -128,6 +130,11 @@ func initAPI(ctx context.Context, k8sClient *k8s.KubernetesAPI, dynamicClient dy
 			if err != nil {
 				return nil, err
 			}
+		case res == ExtWorkload:
+			err := k8s.ExtWorkloadAccess(ctx, k8sClient)
+			if err != nil {
+				return nil, err
+			}
 		default:
 			continue
 		}
@@ -230,6 +237,13 @@ func newAPI(
 			api.es = sharedInformers.Discovery().V1().EndpointSlices()
 			api.syncChecks = append(api.syncChecks, api.es.Informer().HasSynced)
 			api.promGauges.addInformerSize(k8s.EndpointSlices, informerLabels, api.es.Informer())
+		case ExtWorkload:
+			if l5dCrdSharedInformers == nil {
+				panic("Linkerd CRD shared informer not configured")
+			}
+			api.ew = l5dCrdSharedInformers.Externalworkload().V1alpha1().ExternalWorkloads()
+			api.syncChecks = append(api.syncChecks, api.ew.Informer().HasSynced)
+			api.promGauges.addInformerSize(k8s.ExtWorkload, informerLabels, api.ew.Informer())
 		case Job:
 			api.job = sharedInformers.Batch().V1().Jobs()
 			api.syncChecks = append(api.syncChecks, api.job.Informer().HasSynced)
@@ -386,6 +400,15 @@ func (api *API) ES() discoveryinformers.EndpointSliceInformer {
 	return api.es
 }
 
+// ExtWorkload() provides access to a shared informer and lister for
+// ExternalWorkload CRDs
+func (api *API) ExtWorkload() ewinformers.ExternalWorkloadInformer {
+	if api.ew == nil {
+		panic("ExternalWorkload informer not configured")
+	}
+	return api.ew
+}
+
 // CM provides access to a shared informer and lister for ConfigMaps.
 func (api *API) CM() coreinformers.ConfigMapInformer {
 	if api.cm == nil {
diff --git a/controller/k8s/api_resource.go b/controller/k8s/api_resource.go
index 8681fd64c..af035e9ad 100644
--- a/controller/k8s/api_resource.go
+++ b/controller/k8s/api_resource.go
@@ -30,6 +30,7 @@ const (
 	DS
 	Endpoint
 	ES // EndpointSlice resource
+	ExtWorkload
 	Job
 	MWC
 	NS
diff --git a/pkg/k8s/authz.go b/pkg/k8s/authz.go
index 5a9c36b94..3d196815a 100644
--- a/pkg/k8s/authz.go
+++ b/pkg/k8s/authz.go
@@ -128,6 +128,24 @@ func ServersAccess(ctx context.Context, k8sClient kubernetes.Interface) error {
 	return errors.New("Server CRD not found")
 }
 
+// ExtWorkloadAccess checks whether the ExternalWorkload CRD is installed on the
+// cluster and the client is authorized to access ExternalWorkloads
+func ExtWorkloadAccess(ctx context.Context, k8sClient kubernetes.Interface) error {
+	groupVersion := fmt.Sprintf("%s/%s", WorkloadAPIGroup, WorkloadAPIVersion)
+	res, err := k8sClient.Discovery().ServerResourcesForGroupVersion(groupVersion)
+	if err != nil {
+		return err
+	}
+	if res.GroupVersion == groupVersion {
+		for _, apiRes := range res.APIResources {
+			if apiRes.Kind == ExtWorkloadKind {
+				return ResourceAuthz(ctx, k8sClient, "", "list", WorkloadAPIGroup, "", "externalworkloads", "")
+			}
+		}
+	}
+	return errors.New("ExternalWorkload CRD not found")
+}
+
 // EndpointSliceAccess verifies whether the K8s cluster has
 // access to EndpointSlice resources.
 func EndpointSliceAccess(ctx context.Context, k8sClient kubernetes.Interface) error {
diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go
index 23c20a0ec..0d94a1bcc 100644
--- a/pkg/k8s/k8s.go
+++ b/pkg/k8s/k8s.go
@@ -19,6 +19,7 @@ const (
 	Deployment            = "deployment"
 	Endpoints             = "endpoints"
 	EndpointSlices        = "endpointslices"
+	ExtWorkload           = "externalworkload"
 	Job                   = "job"
 	MeshTLSAuthentication = "meshtlsauthentication"
 	MutatingWebhookConfig = "mutatingwebhookconfig"
@@ -50,9 +51,13 @@ const (
 
 	K8sCoreAPIGroup = "core"
 
-	NamespaceKind = "Namespace"
-	ServerKind    = "Server"
-	HTTPRouteKind = "HTTPRoute"
+	NamespaceKind   = "Namespace"
+	ServerKind      = "Server"
+	HTTPRouteKind   = "HTTPRoute"
+	ExtWorkloadKind = "ExternalWorkload"
+
+	WorkloadAPIGroup   = "workload.linkerd.io"
+	WorkloadAPIVersion = "v1alpha1"
 
 	// special case k8s job label, to not conflict with Prometheus' job label
 	l5dJob = "k8s_job"
diff --git a/policy-controller/k8s/api/src/external_workload.rs b/policy-controller/k8s/api/src/external_workload.rs
new file mode 100644
index 000000000..92cdf79c0
--- /dev/null
+++ b/policy-controller/k8s/api/src/external_workload.rs
@@ -0,0 +1,95 @@
+use kube::CustomResource;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+/// ExternalWorkload describes a single workload (i.e. a deployable unit,
+/// conceptually similar to a Kubernetes Pod) that is running outside of a
+/// Kubernetes cluster. An ExternalWorkload should be enrolled in the mesh and
+/// typically represents a virtual machine.
+#[derive(Clone, Debug, PartialEq, Eq, CustomResource, Deserialize, Serialize, JsonSchema)]
+#[kube(
+    group = "workload.linkerd.io",
+    version = "v1alpha1",
+    kind = "ExternalWorkload",
+    status = "ExternalWorkloadStatus",
+    namespaced
+)]
+pub struct ExternalWorkloadSpec {
+    /// MeshTls describes TLS settings associated with an external workload
+    #[serde(rename = "meshTls")]
+    pub mesh_tls: Option<MeshTls>,
+    /// Ports describes a set of ports exposed by the workload
+    pub ports: Vec<PortSpec>,
+    /// List of IP addresses that can be used to send traffic to an external
+    /// workload
+    #[serde(rename = "workloadIPs")]
+    pub workload_ips: Vec<WorkloadIP>,
+}
+
+/// MeshTls describes TLS settings associated with an external workload
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
+pub struct MeshTls {
+    /// Identity associated with the workload. Used by peers to perform
+    /// verification in the mTLS handshake
+    pub identity: Option<String>,
+    /// ServerName is the DNS formatted name associated with the workload. Used
+    /// to terminate TLS using the SNI extension.
+    #[serde(rename = "serverName")]
+    pub server_name: Option<String>,
+}
+
+/// PortSpec represents a network port in a single workload.
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
+pub struct PortSpec {
+    /// If specified, must be an IANA_SVC_NAME and unique within the exposed
+    /// ports set. Each named port must have a unique name. The name may be
+    /// referred to by services
+    pub name: Option<String>,
+    /// Number of port exposed on the workload's IP address.
+    /// Must be a valid port number, i.e. 0 < x < 65536.
+    pub port: std::num::NonZeroU16,
+    /// Protocol defines network protocols supported. One of UDP, TCP, or SCTP.
+    /// Should coincide with Service selecting the workload.
+    /// Defaults to "TCP" if unspecified.
+    pub protocol: Option<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
+pub struct ExternalWorkloadStatus {
+    pub conditions: Vec<Condition>,
+}
+
+/// WorkloadIPs contains a list of IP addresses exposed by an ExternalWorkload
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
+pub struct WorkloadIP {
+    pub ip: String,
+}
+
+/// WorkloadCondition represents the service state of an ExternalWorkload
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct Condition {
+    /// Type of the condition
+    // see: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#pod-conditions
+    #[serde(rename = "type")]
+    typ: String,
+    /// Status of the condition.
+    /// Can be True, False, Unknown
+    status: ConditionStatus,
+    /// Last time a condition transitioned from one status to another.
+    last_transition_time: crate::apimachinery::pkg::apis::meta::v1::Time,
+    /// Last time an ExternalWorkload was probed for a condition.
+    last_probe_time: Option<crate::apimachinery::pkg::apis::meta::v1::Time>,
+    /// Unique one word reason in CamelCase that describes the reason for a
+    /// transition.
+    reason: String,
+    /// Human readable message that describes details about last transition.
+    message: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
+pub enum ConditionStatus {
+    True,
+    False,
+    Unknown,
+}
diff --git a/policy-controller/k8s/api/src/lib.rs b/policy-controller/k8s/api/src/lib.rs
index d21167a9f..db3f526a2 100644
--- a/policy-controller/k8s/api/src/lib.rs
+++ b/policy-controller/k8s/api/src/lib.rs
@@ -2,6 +2,7 @@
 #![forbid(unsafe_code)]
 
 pub mod duration;
+pub mod external_workload;
 pub mod labels;
 pub mod policy;
 
diff --git a/policy-controller/k8s/api/src/policy/server.rs b/policy-controller/k8s/api/src/policy/server.rs
index 5c40f1459..82a68ec75 100644
--- a/policy-controller/k8s/api/src/policy/server.rs
+++ b/policy-controller/k8s/api/src/policy/server.rs
@@ -8,17 +8,26 @@ use std::{fmt, num::NonZeroU16};
 #[derive(Clone, Debug, PartialEq, Eq, CustomResource, Deserialize, Serialize, JsonSchema)]
 #[kube(
     group = "policy.linkerd.io",
-    version = "v1beta1",
+    version = "v1beta2",
     kind = "Server",
     namespaced
 )]
 #[serde(rename_all = "camelCase")]
 pub struct ServerSpec {
-    pub pod_selector: labels::Selector,
+    #[serde(flatten)]
+    pub selector: Selector,
     pub port: Port,
     pub proxy_protocol: Option<ProxyProtocol>,
 }
 
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
+pub enum Selector {
+    #[serde(rename = "podSelector")]
+    Pod(labels::Selector),
+    #[serde(rename = "externalWorkloadSelector")]
+    ExternalWorkload(labels::Selector),
+}
+
 /// References a pod spec's port by name or number.
 #[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
 #[serde(untagged)]
diff --git a/policy-controller/k8s/index/src/inbound/index.rs b/policy-controller/k8s/index/src/inbound/index.rs
index 576d92d34..8b94c1c49 100644
--- a/policy-controller/k8s/index/src/inbound/index.rs
+++ b/policy-controller/k8s/index/src/inbound/index.rs
@@ -25,7 +25,9 @@ use linkerd_policy_controller_core::{
     },
     IdentityMatch, Ipv4Net, Ipv6Net, NetworkMatch,
 };
-use linkerd_policy_controller_k8s_api::{self as k8s, policy::server::Port, ResourceExt};
+use linkerd_policy_controller_k8s_api::{
+    self as k8s, policy::server::Port, policy::server::Selector, ResourceExt,
+};
 use parking_lot::RwLock;
 use std::{
     collections::{hash_map::Entry, BTreeSet},
@@ -924,34 +926,36 @@ impl Pod {
         );
 
         for (srvname, server) in policy.servers.iter() {
-            if server.pod_selector.matches(&self.meta.labels) {
-                for port in self.select_ports(&server.port_ref).into_iter() {
-                    // If the port is already matched to a server, then log a warning and skip
-                    // updating it so it doesn't flap between servers.
-                    if let Some(prior) = matched_ports.get(&port) {
-                        tracing::warn!(
-                            port = %port,
-                            server = %prior,
-                            conflict = %srvname,
-                            "Port already matched by another server; skipping"
-                        );
-                        continue;
-                    }
+            if let Selector::Pod(pod_selector) = &server.selector {
+                if pod_selector.matches(&self.meta.labels) {
+                    for port in self.select_ports(&server.port_ref).into_iter() {
+                        // If the port is already matched to a server, then log a warning and skip
+                        // updating it so it doesn't flap between servers.
+                        if let Some(prior) = matched_ports.get(&port) {
+                            tracing::warn!(
+                                port = %port,
+                                server = %prior,
+                                conflict = %srvname,
+                                "Port already matched by another server; skipping"
+                            );
+                            continue;
+                        }
 
-                    let s = policy.inbound_server(
-                        srvname.clone(),
-                        server,
-                        authentications,
-                        self.probes
-                            .get(&port)
-                            .into_iter()
-                            .flatten()
-                            .map(|p| p.as_str()),
-                    );
-                    self.update_server(port, srvname, s);
+                        let s = policy.inbound_server(
+                            srvname.clone(),
+                            server,
+                            authentications,
+                            self.probes
+                                .get(&port)
+                                .into_iter()
+                                .flatten()
+                                .map(|p| p.as_str()),
+                        );
+                        self.update_server(port, srvname, s);
 
-                    matched_ports.insert(port, srvname.clone());
-                    unmatched_ports.remove(&port);
+                        matched_ports.insert(port, srvname.clone());
+                        unmatched_ports.remove(&port);
+                    }
                 }
             }
         }
diff --git a/policy-controller/k8s/index/src/inbound/server.rs b/policy-controller/k8s/index/src/inbound/server.rs
index 67ca821b6..d017655e0 100644
--- a/policy-controller/k8s/index/src/inbound/server.rs
+++ b/policy-controller/k8s/index/src/inbound/server.rs
@@ -1,12 +1,14 @@
 use crate::ClusterInfo;
 use linkerd_policy_controller_core::inbound::ProxyProtocol;
-use linkerd_policy_controller_k8s_api::{self as k8s, policy::server::Port};
+use linkerd_policy_controller_k8s_api::{
+    self as k8s, policy::server::Port, policy::server::Selector,
+};
 
 /// The parts of a `Server` resource that can change.
 #[derive(Debug, PartialEq)]
 pub(crate) struct Server {
     pub labels: k8s::Labels,
-    pub pod_selector: k8s::labels::Selector,
+    pub selector: Selector,
     pub port_ref: Port,
     pub protocol: ProxyProtocol,
 }
@@ -15,7 +17,7 @@ impl Server {
     pub(crate) fn from_resource(srv: k8s::policy::Server, cluster: &ClusterInfo) -> Self {
         Self {
             labels: srv.metadata.labels.into(),
-            pod_selector: srv.spec.pod_selector,
+            selector: srv.spec.selector,
             port_ref: srv.spec.port,
             protocol: proxy_protocol(srv.spec.proxy_protocol, cluster),
         }
diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs
index 63eb41cf2..06ceafbb3 100644
--- a/policy-controller/src/admission.rs
+++ b/policy-controller/src/admission.rs
@@ -1,12 +1,9 @@
 use super::validation;
-use crate::k8s::{
-    labels,
-    policy::{
-        httproute, AuthorizationPolicy, AuthorizationPolicySpec, HttpRoute, HttpRouteSpec,
-        LocalTargetRef, MeshTLSAuthentication, MeshTLSAuthenticationSpec, NamespacedTargetRef,
-        NetworkAuthentication, NetworkAuthenticationSpec, Server, ServerAuthorization,
-        ServerAuthorizationSpec, ServerSpec,
-    },
+use crate::k8s::policy::{
+    httproute, server::Selector, AuthorizationPolicy, AuthorizationPolicySpec, HttpRoute,
+    HttpRouteSpec, LocalTargetRef, MeshTLSAuthentication, MeshTLSAuthenticationSpec,
+    NamespacedTargetRef, NetworkAuthentication, NetworkAuthenticationSpec, Server,
+    ServerAuthorization, ServerAuthorizationSpec, ServerSpec,
 };
 use anyhow::{anyhow, bail, ensure, Result};
 use futures::future;
@@ -328,7 +325,7 @@ impl Validate<ServerSpec> for Admission {
             let server_name = server.name_unchecked();
             if server_name != name
                 && server.spec.port == spec.port
-                && Self::overlaps(&server.spec.pod_selector, &spec.pod_selector)
+                && Self::overlaps(&server.spec.selector, &spec.selector)
             {
                 let server_ns = server.namespace();
                 let server_ns = server_ns.as_deref().unwrap_or("default");
@@ -351,9 +348,19 @@ impl Admission {
     // example, if `left` selects pods with 'foo=bar' and `right` selects pods with
     // 'foo', we should indicate the selectors overlap. It's a bit tricky to work
     // through all of the cases though, so we'll just punt for now.
-    fn overlaps(left: &labels::Selector, right: &labels::Selector) -> bool {
-        if left.selects_all() || right.selects_all() {
-            return true;
+    fn overlaps(left: &Selector, right: &Selector) -> bool {
+        match (left, right) {
+            (Selector::Pod(ps_left), Selector::Pod(ps_right)) => {
+                if ps_left.selects_all() || ps_right.selects_all() {
+                    return true;
+                }
+            }
+            (Selector::ExternalWorkload(et_left), Selector::ExternalWorkload(et_right)) => {
+                if et_left.selects_all() || et_right.selects_all() {
+                    return true;
+                }
+            }
+            (_, _) => return false,
         }
 
         left == right
diff --git a/web/app/package.json b/web/app/package.json
index 3798e8dd6..b5b1d81d6 100644
--- a/web/app/package.json
+++ b/web/app/package.json
@@ -22,7 +22,7 @@
     "d3-force": "3.0.0",
     "d3-format": "3.1.0",
     "d3-selection": "3.0.0",
-    "date-fns": "3.0.6",
+    "date-fns": "3.1.0",
     "jest-progress-bar-reporter": "^1.0.25",
     "locales-detector": "3.0.2",
     "lodash": "4.17.21",
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index 493933218..87dab3c7f 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -3440,10 +3440,10 @@ data-urls@^3.0.2:
     whatwg-mimetype "^3.0.0"
     whatwg-url "^11.0.0"
 
-date-fns@3.0.6:
-  version "3.0.6"
-  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.0.6.tgz#fe3aeb7592d359f075ffc41cb16139828810ca83"
-  integrity sha512-W+G99rycpKMMF2/YD064b2lE7jJGUe+EjOES7Q8BIGY8sbNdbgcs9XFTZwvzc9Jx1f3k7LB7gZaZa7f8Agzljg==
+date-fns@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.1.0.tgz#576df8df93202942aeebd49ecc042218feb40426"
+  integrity sha512-ZO7yefXV/wCWzd3I9haCHmfzlfA3i1a2HHO7ZXjtJrRjXt8FULKJ2Vl8wji3XYF4dQ0ZJ/tokXDZeYlFvgms9Q==
 
 date-fns@^2.16.1:
   version "2.30.0"


Test patch:
diff --git a/cli/cmd/endpoints_test.go b/cli/cmd/endpoints_test.go
index 28acfba77..e30e90aa7 100644
--- a/cli/cmd/endpoints_test.go
+++ b/cli/cmd/endpoints_test.go
@@ -116,6 +116,8 @@ func TestEndpoints(t *testing.T) {
 }
 
 func testEndpointsCall(exp endpointsExp, t *testing.T) {
+	t.Helper()
+
 	updates := make([]pb.Update, 0)
 	for _, endpoint := range exp.endpoints {
 		addrSet := util.BuildAddrSet(endpoint)
diff --git a/cli/cmd/testdata/endpoints_one_output_json.golden b/cli/cmd/testdata/endpoints_one_output_json.golden
index 669a03893..912a7da56 100644
--- a/cli/cmd/testdata/endpoints_one_output_json.golden
+++ b/cli/cmd/testdata/endpoints_one_output_json.golden
@@ -4,13 +4,21 @@
     "ip": "1.2.3.4",
     "port": 8080,
     "pod": "emoji-6bf9f47bd5-jjcrl",
-    "service": "emoji-svc.emojivoto"
+    "service": "emoji-svc.emojivoto",
+    "weight": 0,
+    "labels": {
+      "pod": "emoji-6bf9f47bd5-jjcrl"
+    }
   },
   {
     "namespace": "emojivoto",
     "ip": "5.6.7.8",
     "port": 8080,
     "pod": "voting-7bf9f47bd5-jjdrl",
-    "service": "voting-svc.emojivoto"
+    "service": "voting-svc.emojivoto",
+    "weight": 0,
+    "labels": {
+      "pod": "voting-7bf9f47bd5-jjdrl"
+    }
   }
 ]
diff --git a/cli/cmd/testdata/install_crds.golden b/cli/cmd/testdata/install_crds.golden
index 9632fe2c7..57722675d 100644
--- a/cli/cmd/testdata/install_crds.golden
+++ b/cli/cmd/testdata/install_crds.golden
@@ -5793,7 +5793,9 @@ spec:
                   default: unknown
     - name: v1beta1
       served: true
-      storage: true
+      storage: false
+      deprecated: true
+      deprecationWarning: "policy.linkerd.io/v1alpha1 Server is deprecated; use policy.linkerd.io/v1beta2 Server"
       schema:
         openAPIV3Schema:
           type: object
@@ -5851,6 +5853,94 @@ spec:
         type: string
         description: The protocol of the server
         jsonPath: .spec.proxyProtocol
+    - name: v1beta2
+      served: true
+      storage: true
+      schema:
+        openAPIV3Schema:
+          type: object
+          required: [spec]
+          properties:
+            spec:
+              type: object
+              required:
+                - port
+              oneOf:
+                - required: [podSelector]
+                - required: [externalWorkloadSelector]
+              properties:
+                podSelector:
+                  type: object
+                  description: >-
+                    Selects pods in the same namespace.
+
+                    The result of matchLabels and matchExpressions are ANDed.
+                    Selects all if empty.
+                  properties:
+                    matchLabels:
+                      type: object
+                      x-kubernetes-preserve-unknown-fields: true
+                    matchExpressions:
+                      type: array
+                      items:
+                        type: object
+                        required: [key, operator]
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                            enum: [In, NotIn, Exists, DoesNotExist]
+                          values:
+                            type: array
+                            items:
+                              type: string
+                externalWorkloadSelector:
+                  type: object
+                  description: >-
+                    Selects ExternalWorkloads in the same namespace.
+
+                    The result of matchLabels and matchExpressions are ANDed.
+                    Selects all if empty.
+                  properties:
+                    matchLabels:
+                      type: object
+                      x-kubernetes-preserve-unknown-fields: true
+                    matchExpressions:
+                      type: array
+                      items:
+                        type: object
+                        required: [key, operator]
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                            enum: [In, NotIn, Exists, DoesNotExist]
+                          values:
+                            type: array
+                            items:
+                              type: string
+                port:
+                  description: >-
+                    A port name or number. Must exist in a pod spec.
+                  x-kubernetes-int-or-string: true
+                proxyProtocol:
+                  description: >-
+                    Configures protocol discovery for inbound connections.
+
+                    Supersedes the `config.linkerd.io/opaque-ports` annotation.
+                  type: string
+                  default: unknown
+      additionalPrinterColumns:
+      - name: Port
+        type: string
+        description: The port the server is listening on
+        jsonPath: .spec.port
+      - name: Protocol
+        type: string
+        description: The protocol of the server
+        jsonPath: .spec.proxyProtocol
 ---
 ###
 ### Service Profile CRD
diff --git a/cli/cmd/testdata/install_helm_crds_output.golden b/cli/cmd/testdata/install_helm_crds_output.golden
index ee66ee8f1..b347c93d2 100644
--- a/cli/cmd/testdata/install_helm_crds_output.golden
+++ b/cli/cmd/testdata/install_helm_crds_output.golden
@@ -5805,7 +5805,9 @@ spec:
                   default: unknown
     - name: v1beta1
       served: true
-      storage: true
+      storage: false
+      deprecated: true
+      deprecationWarning: "policy.linkerd.io/v1alpha1 Server is deprecated; use policy.linkerd.io/v1beta2 Server"
       schema:
         openAPIV3Schema:
           type: object
@@ -5863,6 +5865,94 @@ spec:
         type: string
         description: The protocol of the server
         jsonPath: .spec.proxyProtocol
+    - name: v1beta2
+      served: true
+      storage: true
+      schema:
+        openAPIV3Schema:
+          type: object
+          required: [spec]
+          properties:
+            spec:
+              type: object
+              required:
+                - port
+              oneOf:
+                - required: [podSelector]
+                - required: [externalWorkloadSelector]
+              properties:
+                podSelector:
+                  type: object
+                  description: >-
+                    Selects pods in the same namespace.
+
+                    The result of matchLabels and matchExpressions are ANDed.
+                    Selects all if empty.
+                  properties:
+                    matchLabels:
+                      type: object
+                      x-kubernetes-preserve-unknown-fields: true
+                    matchExpressions:
+                      type: array
+                      items:
+                        type: object
+                        required: [key, operator]
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                            enum: [In, NotIn, Exists, DoesNotExist]
+                          values:
+                            type: array
+                            items:
+                              type: string
+                externalWorkloadSelector:
+                  type: object
+                  description: >-
+                    Selects ExternalWorkloads in the same namespace.
+
+                    The result of matchLabels and matchExpressions are ANDed.
+                    Selects all if empty.
+                  properties:
+                    matchLabels:
+                      type: object
+                      x-kubernetes-preserve-unknown-fields: true
+                    matchExpressions:
+                      type: array
+                      items:
+                        type: object
+                        required: [key, operator]
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                            enum: [In, NotIn, Exists, DoesNotExist]
+                          values:
+                            type: array
+                            items:
+                              type: string
+                port:
+                  description: >-
+                    A port name or number. Must exist in a pod spec.
+                  x-kubernetes-int-or-string: true
+                proxyProtocol:
+                  description: >-
+                    Configures protocol discovery for inbound connections.
+
+                    Supersedes the `config.linkerd.io/opaque-ports` annotation.
+                  type: string
+                  default: unknown
+      additionalPrinterColumns:
+      - name: Port
+        type: string
+        description: The port the server is listening on
+        jsonPath: .spec.port
+      - name: Protocol
+        type: string
+        description: The protocol of the server
+        jsonPath: .spec.proxyProtocol
 ---
 # Source: linkerd-crds/templates/serviceprofile.yaml
 ---
diff --git a/cli/cmd/testdata/install_helm_crds_output_ha.golden b/cli/cmd/testdata/install_helm_crds_output_ha.golden
index ee66ee8f1..b347c93d2 100644
--- a/cli/cmd/testdata/install_helm_crds_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_crds_output_ha.golden
@@ -5805,7 +5805,9 @@ spec:
                   default: unknown
     - name: v1beta1
       served: true
-      storage: true
+      storage: false
+      deprecated: true
+      deprecationWarning: "policy.linkerd.io/v1alpha1 Server is deprecated; use policy.linkerd.io/v1beta2 Server"
       schema:
         openAPIV3Schema:
           type: object
@@ -5863,6 +5865,94 @@ spec:
         type: string
         description: The protocol of the server
         jsonPath: .spec.proxyProtocol
+    - name: v1beta2
+      served: true
+      storage: true
+      schema:
+        openAPIV3Schema:
+          type: object
+          required: [spec]
+          properties:
+            spec:
+              type: object
+              required:
+                - port
+              oneOf:
+                - required: [podSelector]
+                - required: [externalWorkloadSelector]
+              properties:
+                podSelector:
+                  type: object
+                  description: >-
+                    Selects pods in the same namespace.
+
+                    The result of matchLabels and matchExpressions are ANDed.
+                    Selects all if empty.
+                  properties:
+                    matchLabels:
+                      type: object
+                      x-kubernetes-preserve-unknown-fields: true
+                    matchExpressions:
+                      type: array
+                      items:
+                        type: object
+                        required: [key, operator]
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                            enum: [In, NotIn, Exists, DoesNotExist]
+                          values:
+                            type: array
+                            items:
+                              type: string
+                externalWorkloadSelector:
+                  type: object
+                  description: >-
+                    Selects ExternalWorkloads in the same namespace.
+
+                    The result of matchLabels and matchExpressions are ANDed.
+                    Selects all if empty.
+                  properties:
+                    matchLabels:
+                      type: object
+                      x-kubernetes-preserve-unknown-fields: true
+                    matchExpressions:
+                      type: array
+                      items:
+                        type: object
+                        required: [key, operator]
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                            enum: [In, NotIn, Exists, DoesNotExist]
+                          values:
+                            type: array
+                            items:
+                              type: string
+                port:
+                  description: >-
+                    A port name or number. Must exist in a pod spec.
+                  x-kubernetes-int-or-string: true
+                proxyProtocol:
+                  description: >-
+                    Configures protocol discovery for inbound connections.
+
+                    Supersedes the `config.linkerd.io/opaque-ports` annotation.
+                  type: string
+                  default: unknown
+      additionalPrinterColumns:
+      - name: Port
+        type: string
+        description: The port the server is listening on
+        jsonPath: .spec.port
+      - name: Protocol
+        type: string
+        description: The protocol of the server
+        jsonPath: .spec.proxyProtocol
 ---
 # Source: linkerd-crds/templates/serviceprofile.yaml
 ---
diff --git a/policy-controller/k8s/index/src/inbound/tests.rs b/policy-controller/k8s/index/src/inbound/tests.rs
index 21e7b176e..fec468d5d 100644
--- a/policy-controller/k8s/index/src/inbound/tests.rs
+++ b/policy-controller/k8s/index/src/inbound/tests.rs
@@ -118,7 +118,7 @@ fn mk_server(
         },
         spec: k8s::policy::ServerSpec {
             port,
-            pod_selector: pod_labels.into_iter().collect(),
+            selector: k8s::policy::server::Selector::Pod(pod_labels.into_iter().collect()),
             proxy_protocol,
         },
     }
diff --git a/policy-controller/k8s/status/src/tests/http_routes.rs b/policy-controller/k8s/status/src/tests/http_routes.rs
index 6f43697a2..468b967cf 100644
--- a/policy-controller/k8s/status/src/tests/http_routes.rs
+++ b/policy-controller/k8s/status/src/tests/http_routes.rs
@@ -172,7 +172,7 @@ fn make_server(
         },
         spec: k8s::policy::ServerSpec {
             port,
-            pod_selector: pod_labels.into_iter().collect(),
+            selector: k8s::policy::server::Selector::Pod(pod_labels.into_iter().collect()),
             proxy_protocol,
         },
     }
diff --git a/policy-test/src/web.rs b/policy-test/src/web.rs
index 0230de582..6b86bd247 100644
--- a/policy-test/src/web.rs
+++ b/policy-test/src/web.rs
@@ -41,7 +41,9 @@ pub fn server(ns: &str) -> k8s::policy::Server {
             ..Default::default()
         },
         spec: k8s::policy::ServerSpec {
-            pod_selector: k8s::labels::Selector::from_iter(Some(("app", "web"))),
+            selector: k8s::policy::server::Selector::Pod(k8s::labels::Selector::from_iter(Some((
+                "app", "web",
+            )))),
             port: k8s::policy::server::Port::Name("http".to_string()),
             proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
         },
diff --git a/policy-test/tests/admit_server.rs b/policy-test/tests/admit_server.rs
index 7f3270c1f..d8dcb34cd 100644
--- a/policy-test/tests/admit_server.rs
+++ b/policy-test/tests/admit_server.rs
@@ -1,6 +1,6 @@
 use linkerd_policy_controller_k8s_api::{
-    self as api, labels,
-    policy::server::{Port, ProxyProtocol, Server, ServerSpec},
+    self as api,
+    policy::server::{Port, ProxyProtocol, Selector, Server, ServerSpec},
 };
 use linkerd_policy_test::{admission, with_temp_ns};
 
@@ -13,7 +13,7 @@ async fn accepts_valid() {
             ..Default::default()
         },
         spec: ServerSpec {
-            pod_selector: api::labels::Selector::default(),
+            selector: Selector::Pod(api::labels::Selector::default()),
             port: Port::Number(80.try_into().unwrap()),
             proxy_protocol: None,
         },
@@ -31,7 +31,7 @@ async fn accepts_server_updates() {
                 ..Default::default()
             },
             spec: ServerSpec {
-                pod_selector: api::labels::Selector::from_iter(Some(("app", "test"))),
+                selector: Selector::Pod(api::labels::Selector::from_iter(Some(("app", "test")))),
                 port: Port::Number(80.try_into().unwrap()),
                 proxy_protocol: None,
             },
@@ -57,7 +57,7 @@ async fn accepts_server_updates() {
 async fn rejects_identitical_pod_selector() {
     with_temp_ns(|client, ns| async move {
         let spec = ServerSpec {
-            pod_selector: api::labels::Selector::from_iter(Some(("app", "test"))),
+            selector: Selector::Pod(api::labels::Selector::from_iter(Some(("app", "test")))),
             port: Port::Number(80.try_into().unwrap()),
             proxy_protocol: None,
         };
@@ -103,7 +103,7 @@ async fn rejects_all_pods_selected() {
                 ..Default::default()
             },
             spec: ServerSpec {
-                pod_selector: api::labels::Selector::from_iter(Some(("app", "test"))),
+                selector: Selector::Pod(api::labels::Selector::from_iter(Some(("app", "test")))),
                 port: Port::Number(80.try_into().unwrap()),
                 proxy_protocol: Some(ProxyProtocol::Http2),
             },
@@ -119,7 +119,7 @@ async fn rejects_all_pods_selected() {
                 ..Default::default()
             },
             spec: ServerSpec {
-                pod_selector: api::labels::Selector::default(),
+                selector: Selector::Pod(api::labels::Selector::default()),
                 port: Port::Number(80.try_into().unwrap()),
                 // proxy protocol doesn't factor into the selection
                 proxy_protocol: Some(ProxyProtocol::Http1),
@@ -151,7 +151,8 @@ async fn rejects_invalid_proxy_protocol() {
     )]
     #[serde(rename_all = "camelCase")]
     pub struct ServerSpec {
-        pub pod_selector: labels::Selector,
+        #[serde(flatten)]
+        pub selector: Selector,
         pub port: Port,
         pub proxy_protocol: String,
     }
@@ -171,7 +172,7 @@ async fn rejects_invalid_proxy_protocol() {
             ..Default::default()
         },
         spec: ServerSpec {
-            pod_selector: api::labels::Selector::default(),
+            selector: Selector::Pod(api::labels::Selector::default()),
             port: Port::Number(80.try_into().unwrap()),
             proxy_protocol: "garbanzo".to_string(),
         },
diff --git a/policy-test/tests/inbound_api.rs b/policy-test/tests/inbound_api.rs
index 12e5008e0..dd15b762d 100644
--- a/policy-test/tests/inbound_api.rs
+++ b/policy-test/tests/inbound_api.rs
@@ -630,7 +630,7 @@ fn mk_admin_server(ns: &str, name: &str) -> k8s::policy::Server {
             ..Default::default()
         },
         spec: k8s::policy::ServerSpec {
-            pod_selector: k8s::labels::Selector::default(),
+            selector: k8s::policy::server::Selector::Pod(k8s::labels::Selector::default()),
             port: k8s::policy::server::Port::Number(4191.try_into().unwrap()),
             proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
         },
diff --git a/policy-test/tests/inbound_http_route_status.rs b/policy-test/tests/inbound_http_route_status.rs
index e461e212d..102a73119 100644
--- a/policy-test/tests/inbound_http_route_status.rs
+++ b/policy-test/tests/inbound_http_route_status.rs
@@ -16,7 +16,9 @@ async fn inbound_accepted_parent() {
                 ..Default::default()
             },
             spec: k8s::policy::ServerSpec {
-                pod_selector: k8s::labels::Selector::from_iter(Some(("app", server_name))),
+                selector: k8s::policy::server::Selector::Pod(k8s::labels::Selector::from_iter(
+                    Some(("app", server_name)),
+                )),
                 port: k8s::policy::server::Port::Name("http".to_string()),
                 proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
             },
@@ -91,7 +93,9 @@ async fn inbound_multiple_parents() {
                 ..Default::default()
             },
             spec: k8s::policy::ServerSpec {
-                pod_selector: k8s::labels::Selector::from_iter(Some(("app", "test-valid-server"))),
+                selector: k8s::policy::server::Selector::Pod(k8s::labels::Selector::from_iter(
+                    Some(("app", "test-valid-server")),
+                )),
                 port: k8s::policy::server::Port::Name("http".to_string()),
                 proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
             },
@@ -182,7 +186,9 @@ async fn inbound_accepted_reconcile_no_parent() {
                 ..Default::default()
             },
             spec: k8s::policy::ServerSpec {
-                pod_selector: k8s::labels::Selector::from_iter(Some(("app", server_name))),
+                selector: k8s::policy::server::Selector::Pod(k8s::labels::Selector::from_iter(
+                    Some(("app", server_name)),
+                )),
                 port: k8s::policy::server::Port::Name("http".to_string()),
                 proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
             },
@@ -229,7 +235,9 @@ async fn inbound_accepted_reconcile_parent_delete() {
                 ..Default::default()
             },
             spec: k8s::policy::ServerSpec {
-                pod_selector: k8s::labels::Selector::from_iter(Some(("app", server_name))),
+                selector: k8s::policy::server::Selector::Pod(k8s::labels::Selector::from_iter(
+                    Some(("app", server_name)),
+                )),
                 port: k8s::policy::server::Port::Name("http".to_string()),
                 proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
             },
diff --git a/test/integration/deep/endpoints/endpoints_test.go b/test/integration/deep/endpoints/endpoints_test.go
index 88f6e6bb6..da34dd418 100644
--- a/test/integration/deep/endpoints/endpoints_test.go
+++ b/test/integration/deep/endpoints/endpoints_test.go
@@ -110,8 +110,10 @@ func createTestCaseTable(controlNs, endpointNs string) []testCase {
     "namespace": "(\S*)",
     "ip": "\d+\.\d+\.\d+\.\d+",
     "port": 8086,
-    "pod": "linkerd\-destination\-[a-f0-9]+\-[a-z0-9]+",
-    "service": "linkerd\-dst\.\S*"
+    "pod": "linkerd-destination\-[a-f0-9]+\-[a-z0-9]+",
+    "service": "linkerd-dst\.\S*",
+    "weight": \d+,
+    "labels": \{(?s).*\}
   \}
 \]`,
 			ns: controlNs,
@@ -124,8 +126,10 @@ func createTestCaseTable(controlNs, endpointNs string) []testCase {
     "namespace": "(\S*)",
     "ip": "\d+\.\d+\.\d+\.\d+",
     "port": 8080,
-    "pod": "linkerd\-identity\-[a-f0-9]+\-[a-z0-9]+",
-    "service": "linkerd\-identity\.\S*"
+    "pod": "linkerd-identity\-[a-f0-9]+\-[a-z0-9]+",
+    "service": "linkerd-identity\.\S*",
+    "weight": \d+,
+    "labels": \{(?s).*\}
   \}
 \]`,
 			ns: controlNs,
@@ -138,8 +142,10 @@ func createTestCaseTable(controlNs, endpointNs string) []testCase {
     "namespace": "(\S*)",
     "ip": "\d+\.\d+\.\d+\.\d+",
     "port": 8443,
-    "pod": "linkerd\-proxy\-injector-[a-f0-9]+\-[a-z0-9]+",
-    "service": "linkerd\-proxy\-injector\.\S*"
+    "pod": "linkerd-proxy-injector-[a-f0-9]+\-[a-z0-9]+",
+    "service": "linkerd-proxy-injector\.\S*",
+    "weight": \d+,
+    "labels": \{(?s).*\}
   \}
 \]`,
 			ns: controlNs,
@@ -152,8 +158,10 @@ func createTestCaseTable(controlNs, endpointNs string) []testCase {
     "namespace": "(\S*)",
     "ip": "\d+\.\d+\.\d+\.\d+",
     "port": 8080,
-    "pod": "nginx\-[a-f0-9]+\-[a-z0-9]+",
-    "service": "nginx\.\S*"
+    "pod": "nginx-[a-f0-9]+\-[a-z0-9]+",
+    "service": "nginx\.\S*",
+    "weight": \d+,
+    "labels": \{(?s).*\}
   \}
 \]`,
 			ns: endpointNs,