Repository: linkerd/linkerd2
Pull request #12098 (2024-02-16 17:31:36)

Rename meshTls to meshTLS in ExternalWorkload CRD
 Rename meshTls to meshTLS in ExternalWorkload CRD

The ExternalWorkload resource we introduced has a minor naming
 inconsistency; `Tls` in `meshTls` is not capitalised. Other resources
 that we have (e.g. authentication resources) capitalise TLS (and so does 
Go, it follows a similar naming convention).

We fix this in the workload resource by changing the field's name and
bumping the version to `v1beta1`.

Upgrading the control plane version will continue to work without
downtime. However, if an existing resource exists, the policy controller
will not completely initialise. It will not enter a crashloop backoff,
but it will also not become ready until the resource is edited or
deleted.

Signed-off-by: Matei David <matei@buoyant.io>


<!--  Thanks for sending a pull request!


If you already have a well-structured git commit message, chances are GitHub
set the title and description of this PR to the git commit message subject and
body, respectively. If so, you may delete these instructions and submit your PR.

If this is your first time, please read our contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md

The title and description of your Pull Request should match the git commit
subject and body, respectively. Git commit messages are structured as follows:

```
Subject

Problem

Solution

Validation

Fixes #[GitHub issue ID]

DCO Sign off
```

Example git commit message:

```
Introduce Pull Request Template

GitHub's community guidelines recommend a pull request template, the repo was
lacking one.

Introduce a `PULL_REQUEST_TEMPLATE.md` file.

Once merged, the
[Community profile checklist](https://github.com/linkerd/linkerd2/community)
should indicate the repo now provides a pull request template.

Fixes #3321

Signed-off-by: Jane Smith <jane.smith@example.com>
```

Note the git commit message subject becomes the pull request title.

For more details around git commits, see the section on Committing in our
contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md#committing
-->



Patch:
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 0acf0e4ce..c572486df 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -149,7 +149,7 @@ jobs:
     timeout-minutes: 5
     steps:
       - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
-      - uses: tj-actions/changed-files@90a06d6ba9543371ab4df8eeca0be07ca6054959
+      - uses: tj-actions/changed-files@3f54ebb830831fc121d3263c1857cfbdc310cdb9
         id: changed
         with:
           files: |
@@ -173,7 +173,7 @@ jobs:
       matrix:
         k8s:
           - v1.22
-          - v1.28
+          - v1.29
     steps:
       - uses: extractions/setup-just@69d82fb0233557aec017ef13706851d0694e0f1d
         env:
@@ -290,7 +290,7 @@ jobs:
     timeout-minutes: 5
     steps:
       - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
-      - uses: tj-actions/changed-files@90a06d6ba9543371ab4df8eeca0be07ca6054959
+      - uses: tj-actions/changed-files@3f54ebb830831fc121d3263c1857cfbdc310cdb9
         id: changed
         with:
           files: |
@@ -334,7 +334,7 @@ jobs:
     timeout-minutes: 5
     steps:
       - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
-      - uses: tj-actions/changed-files@90a06d6ba9543371ab4df8eeca0be07ca6054959
+      - uses: tj-actions/changed-files@3f54ebb830831fc121d3263c1857cfbdc310cdb9
         id: changed
         with:
           files: |
@@ -355,7 +355,7 @@ jobs:
       matrix:
         k8s:
           - v1.22
-          - v1.28
+          - v1.29
     steps:
       - uses: extractions/setup-just@69d82fb0233557aec017ef13706851d0694e0f1d
         env:
diff --git a/.proxy-version b/.proxy-version
index ae0690977..f164f124c 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.220.0
+v2.221.0
diff --git a/CHANGES.md b/CHANGES.md
index f9989b2bc..3d968e92b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,21 @@
 # Changes
 
+## edge-24.2.3
+
+* Allowed the `MutatingWebhookConfig` timeout value to be configured ([#12028])
+  (thanks @mikebell90)
+* Added a counter for items dropped from destination controller workqueue
+  ([#12079])
+* Fixed a spurious `linkerd check` error when using container images with
+  digests ([#12059])
+* Fixed an issue where inbound policy could be incorrect after certain policy
+  resources are deleted ([#12088])
+
+[#12028]: https://github.com/linkerd/linkerd2/pull/12028
+[#12079]: https://github.com/linkerd/linkerd2/pull/12079
+[#12059]: https://github.com/linkerd/linkerd2/pull/12059
+[#12088]: https://github.com/linkerd/linkerd2/pull/12088
+
 ## edge-24.2.2
 
 This release addresses some issues in the destination service that could cause
diff --git a/Cargo.lock b/Cargo.lock
index d3882cd1d..414eee77a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -592,9 +592,9 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.11"
+version = "0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
+checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
 dependencies = [
  "cfg-if",
  "libc",
diff --git a/bin/update-codegen.sh b/bin/update-codegen.sh
index 522413845..091cc7cc5 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 externalworkload:v1alpha1)
+crds=(serviceprofile:v1alpha2 server:v1beta1 serverauthorization:v1beta1 link:v1alpha1 policy:v1alpha1 policy:v1beta3 externalworkload:v1beta1)
 for crd in "${crds[@]}"
 do
   crd_path=$(tr : / <<< "$crd")
diff --git a/charts/linkerd-control-plane/Chart.yaml b/charts/linkerd-control-plane/Chart.yaml
index 3d6f759ea..1ec734ac1 100644
--- a/charts/linkerd-control-plane/Chart.yaml
+++ b/charts/linkerd-control-plane/Chart.yaml
@@ -16,7 +16,7 @@ dependencies:
   - name: partials
     version: 0.1.0
     repository: file://../partials
-version: 1.18.8-edge
+version: 1.18.9-edge
 icon: https://linkerd.io/images/logo-only-200h.png
 maintainers:
   - name: Linkerd authors
diff --git a/charts/linkerd-control-plane/README.md b/charts/linkerd-control-plane/README.md
index e121ce288..90d6203dc 100644
--- a/charts/linkerd-control-plane/README.md
+++ b/charts/linkerd-control-plane/README.md
@@ -3,7 +3,7 @@
 Linkerd gives you observability, reliability, and security
 for your microservices — with no code change required.
 
-![Version: 1.18.8-edge](https://img.shields.io/badge/Version-1.18.8--edge-informational?style=flat-square)
+![Version: 1.18.9-edge](https://img.shields.io/badge/Version-1.18.9--edge-informational?style=flat-square)
 ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)
 ![AppVersion: edge-XX.X.X](https://img.shields.io/badge/AppVersion-edge--XX.X.X-informational?style=flat-square)
 
diff --git a/charts/linkerd-crds/templates/workload/external-workload.yaml b/charts/linkerd-crds/templates/workload/external-workload.yaml
index 9dfcd6193..56cb3bddb 100644
--- a/charts/linkerd-crds/templates/workload/external-workload.yaml
+++ b/charts/linkerd-crds/templates/workload/external-workload.yaml
@@ -20,14 +20,147 @@ spec:
     shortNames: []
   scope: Namespaced
   versions:
-  - additionalPrinterColumns:
+  - name: v1alpha1
+    served: true
+    storage: false
+    schema:
+      openAPIV3Schema:
+        description: >-
+          An ExternalWorkload describes a single workload (i.e. a deployable unit) external
+          to the cluster that should be enrolled in the mesh.
+        type: object
+        required: [spec]
+        properties:
+          apiVerson:
+            type: string
+          kind:
+            type: string
+          metadata:
+            type: object
+          spec:
+            properties:
+              meshTls:
+                description: meshTls describes TLS settings associated with an
+                  external workload.
+                properties:
+                  identity:
+                    type: string
+                    description: identity of the workload. Corresponds to the
+                      identity used in the workload's certificate. It is used
+                      by peers to perform verification in the mTLS handshake.
+                    minLength: 1
+                    maxLength: 253
+                  serverName:
+                    type: string
+                    description: serverName is the name of the workload in DNS
+                      format. It is used by the workload to terminate TLS using
+                      SNI.
+                    minLength: 1
+                    maxLength: 253
+                type: object
+                required:
+                - identity
+                - serverName
+              ports:
+                type: array
+                description: ports describes a list of ports exposed by the
+                  workload
+                items:
+                  properties:
+                    name:
+                      type: string
+                      description: name must be an IANA_SVC_NAME and unique
+                        within the ports set. Each named port can be referred
+                        to by services.
+                    port:
+                      format: int32
+                      maximum: 65535
+                      minimum: 1
+                      type: integer
+                    protocol:
+                      description: protocol exposed by the port. Must be UDP or
+                        TCP. Defaults to TCP.
+                      type: string
+                      default: "TCP"
+                  type: object
+                  required:
+                  - port
+              workloadIPs:
+                type: array
+                description: workloadIPs contains a list of IP addresses that
+                  can be used to send traffic to the workload.
+                items:
+                  type: object
+                  properties:
+                    ip:
+                      type: string
+                # TODO: relax this in the future when ipv6 is supported
+                # an external workload (like a pod) should only
+                # support 2 interfaces
+                maxItems: 1
+            type: object
+            required:
+            - meshTls
+          status:
+            type: object
+            properties:
+              conditions:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    lastProbeTime:
+                      description: lastProbeTime is the last time the
+                        healthcheck endpoint was probed.
+                      format: date-time
+                      type: string
+                    lastTransitionTime:
+                      description: lastTransitionTime is the last time the
+                        condition transitioned from one status to another.
+                      format: date-time
+                      type: string
+                    status:
+                      description: status of the condition (one of True, False, Unknown)
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of the condition in CamelCase or in
+                        foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                    reason:
+                      description: reason contains a programmatic identifier
+                        indicating the reason for the condition's last
+                        transition. Producers of specific condition types may
+                        define expected values and meanings for this field, and
+                        whether the values are considered a guaranteed API. The
+                        value should be a CamelCase string. This field may not
+                        be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    message:
+                      description: message is a human readable message
+                        indicating details about the transition. This may be an
+                        empty string.
+                      maxLength: 32768
+                      type: string
+                required:
+                - status
+                - type
+    additionalPrinterColumns:
     - jsonPath: .spec.meshTls.identity
       name: Identity
       type: string
     - jsonPath: .metadata.creationTimestamp
       name: Age
       type: date
-    name: v1alpha1
+  - name: v1beta1
     served: true
     storage: true
     schema:
@@ -46,8 +179,8 @@ spec:
             type: object
           spec:
             properties:
-              meshTls:
-                description: meshTls describes TLS settings associated with an
+              meshTLS:
+                description: meshTLS describes TLS settings associated with an
                   external workload.
                 properties:
                   identity:
@@ -107,7 +240,7 @@ spec:
                 maxItems: 1
             type: object
             required:
-            - meshTls
+            - meshTLS
           status:
             type: object
             properties:
@@ -160,3 +293,10 @@ spec:
                 required:
                 - status
                 - type
+    additionalPrinterColumns:
+    - jsonPath: .spec.meshTLS.identity
+      name: Identity
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
diff --git a/controller/api/destination/endpoint_translator.go b/controller/api/destination/endpoint_translator.go
index 62ce7cf5e..83ea64b92 100644
--- a/controller/api/destination/endpoint_translator.go
+++ b/controller/api/destination/endpoint_translator.go
@@ -9,7 +9,7 @@ import (
 	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
 	"github.com/linkerd/linkerd2-proxy-api/go/net"
 	"github.com/linkerd/linkerd2/controller/api/destination/watcher"
-	ewv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	ewv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	"github.com/linkerd/linkerd2/pkg/addr"
 	pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
@@ -537,11 +537,11 @@ func createWeightedAddrForExternalWorkload(
 	weightedAddr.TlsIdentity = &pb.TlsIdentity{
 		Strategy: &pb.TlsIdentity_UriLikeIdentity_{
 			UriLikeIdentity: &pb.TlsIdentity_UriLikeIdentity{
-				Uri: address.ExternalWorkload.Spec.MeshTls.Identity,
+				Uri: address.ExternalWorkload.Spec.MeshTLS.Identity,
 			},
 		},
 		ServerName: &pb.TlsIdentity_DnsLikeIdentity{
-			Name: address.ExternalWorkload.Spec.MeshTls.ServerName,
+			Name: address.ExternalWorkload.Spec.MeshTLS.ServerName,
 		},
 	}
 
@@ -689,7 +689,7 @@ func getInboundPort(podSpec *corev1.PodSpec) (uint32, error) {
 
 // getInboundPortFromExternalWorkload gets the inbound port from the ExternalWorkload spec
 // variable.
-func getInboundPortFromExternalWorkload(ewSpec *ewv1alpha1.ExternalWorkloadSpec) (uint32, error) {
+func getInboundPortFromExternalWorkload(ewSpec *ewv1beta1.ExternalWorkloadSpec) (uint32, error) {
 	for _, p := range ewSpec.Ports {
 		if p.Name == pkgK8s.ProxyPortName {
 			return uint32(p.Port), nil
diff --git a/controller/api/destination/external-workload/controller_util.go b/controller/api/destination/external-workload/controller_util.go
index 98937174d..cc0f475e6 100644
--- a/controller/api/destination/external-workload/controller_util.go
+++ b/controller/api/destination/external-workload/controller_util.go
@@ -3,7 +3,7 @@ package externalworkload
 import (
 	"reflect"
 
-	ewv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	ewv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	discoveryv1 "k8s.io/api/discovery/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/util/sets"
@@ -11,8 +11,8 @@ import (
 )
 
 func (ec *EndpointsController) getServicesToUpdateOnExternalWorkloadChange(old, cur interface{}) sets.Set[string] {
-	newEw, newEwOk := cur.(*ewv1alpha1.ExternalWorkload)
-	oldEw, oldEwOk := old.(*ewv1alpha1.ExternalWorkload)
+	newEw, newEwOk := cur.(*ewv1beta1.ExternalWorkload)
+	oldEw, oldEwOk := old.(*ewv1beta1.ExternalWorkload)
 
 	if !oldEwOk {
 		ec.log.Errorf("Expected (cur) to be an EndpointSlice in getServicesToUpdateOnExternalWorkloadChange(), got type: %T", cur)
@@ -68,7 +68,7 @@ func determineNeededServiceUpdates(oldServices, services sets.Set[string], specC
 // getExternalWorkloadSvcMembership accepts a pointer to an external workload
 // resource and returns a set of service keys (<namespace>/<name>). The set
 // includes all services local to the workload's namespace that match the workload.
-func (ec *EndpointsController) getExternalWorkloadSvcMembership(workload *ewv1alpha1.ExternalWorkload) (sets.Set[string], error) {
+func (ec *EndpointsController) getExternalWorkloadSvcMembership(workload *ewv1beta1.ExternalWorkload) (sets.Set[string], error) {
 	keys := sets.Set[string]{}
 	services, err := ec.k8sAPI.Svc().Lister().Services(workload.Namespace).List(labels.Everything())
 	if err != nil {
@@ -120,8 +120,8 @@ func (ec *EndpointsController) getEndpointSliceFromDeleteAction(obj interface{})
 }
 
 // getExternalWorkloadFromDeleteAction parses an ExternalWorkload from a delete action.
-func (ec *EndpointsController) getExternalWorkloadFromDeleteAction(obj interface{}) *ewv1alpha1.ExternalWorkload {
-	if ew, ok := obj.(*ewv1alpha1.ExternalWorkload); ok {
+func (ec *EndpointsController) getExternalWorkloadFromDeleteAction(obj interface{}) *ewv1beta1.ExternalWorkload {
+	if ew, ok := obj.(*ewv1beta1.ExternalWorkload); ok {
 		return ew
 	}
 
@@ -132,7 +132,7 @@ func (ec *EndpointsController) getExternalWorkloadFromDeleteAction(obj interface
 		return nil
 	}
 
-	ew, ok := tombstone.Obj.(*ewv1alpha1.ExternalWorkload)
+	ew, ok := tombstone.Obj.(*ewv1beta1.ExternalWorkload)
 	if !ok {
 		ec.log.Errorf("tombstone contained object that is not a ExternalWorkload: %#v", obj)
 		return nil
@@ -143,7 +143,7 @@ func (ec *EndpointsController) getExternalWorkloadFromDeleteAction(obj interface
 // ewEndpointsChanged returns two boolean values. The first is true if the ExternalWorkload has
 // changed in a way that may change existing endpoints. The second value is true if the
 // ExternalWorkload has changed in a way that may affect which Services it matches.
-func ewEndpointsChanged(oldEw, newEw *ewv1alpha1.ExternalWorkload) (bool, bool) {
+func ewEndpointsChanged(oldEw, newEw *ewv1beta1.ExternalWorkload) (bool, bool) {
 	// Check if the ExternalWorkload labels have changed, indicating a possible
 	// change in the service membership
 	labelsChanged := false
@@ -179,7 +179,7 @@ func ewEndpointsChanged(oldEw, newEw *ewv1alpha1.ExternalWorkload) (bool, bool)
 	}
 
 	// Determine if the ports have changed between workload resources
-	portSet := make(map[int32]ewv1alpha1.PortSpec)
+	portSet := make(map[int32]ewv1beta1.PortSpec)
 	for _, ps := range newEw.Spec.Ports {
 		portSet[ps.Port] = ps
 	}
@@ -211,7 +211,7 @@ func managedByChanged(endpointSlice1, endpointSlice2 *discoveryv1.EndpointSlice)
 	return managedByController(endpointSlice1) != managedByController(endpointSlice2)
 }
 
-func IsEwReady(ew *ewv1alpha1.ExternalWorkload) bool {
+func IsEwReady(ew *ewv1beta1.ExternalWorkload) bool {
 	if len(ew.Status.Conditions) == 0 {
 		return false
 	}
@@ -222,7 +222,7 @@ func IsEwReady(ew *ewv1alpha1.ExternalWorkload) bool {
 		cond := ew.Status.Conditions[i]
 		// Stop once we find a 'Ready' condition. We expect a resource to only
 		// have one 'Ready' type condition.
-		if cond.Type == ewv1alpha1.WorkloadReady && cond.Status == ewv1alpha1.ConditionTrue {
+		if cond.Type == ewv1beta1.WorkloadReady && cond.Status == ewv1beta1.ConditionTrue {
 			return true
 		}
 	}
diff --git a/controller/api/destination/external-workload/endpoints_controller.go b/controller/api/destination/external-workload/endpoints_controller.go
index 94866f15a..8896c3384 100644
--- a/controller/api/destination/external-workload/endpoints_controller.go
+++ b/controller/api/destination/external-workload/endpoints_controller.go
@@ -7,7 +7,7 @@ import (
 	"sync"
 	"time"
 
-	ewv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	ewv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	logging "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
@@ -502,7 +502,7 @@ func (ec *EndpointsController) queueServiceForEndpointSlice(endpointSlice *disco
 }
 
 func (ec *EndpointsController) onAddExternalWorkload(obj interface{}) {
-	ew, ok := obj.(*ewv1alpha1.ExternalWorkload)
+	ew, ok := obj.(*ewv1beta1.ExternalWorkload)
 	if !ok {
 		ec.log.Errorf("couldn't get ExternalWorkload from object %#v", obj)
 		return
diff --git a/controller/api/destination/external-workload/endpoints_reconciler.go b/controller/api/destination/external-workload/endpoints_reconciler.go
index 01b9bac79..9c62fde27 100644
--- a/controller/api/destination/external-workload/endpoints_reconciler.go
+++ b/controller/api/destination/external-workload/endpoints_reconciler.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"sort"
 
-	ewv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	ewv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	logging "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
@@ -70,7 +70,7 @@ func newEndpointsReconciler(k8sAPI *k8s.API, controllerName string, maxEndpoints
 // * Determine what address types the service supports
 // * For each address type, it will determine which slices to process (an
 // EndpointSlice is specialised and supports only one type)
-func (r *endpointsReconciler) reconcile(svc *corev1.Service, ews []*ewv1alpha1.ExternalWorkload, existingSlices []*discoveryv1.EndpointSlice) error {
+func (r *endpointsReconciler) reconcile(svc *corev1.Service, ews []*ewv1beta1.ExternalWorkload, existingSlices []*discoveryv1.EndpointSlice) error {
 	toDelete := []*discoveryv1.EndpointSlice{}
 	slicesByAddrType := make(map[discoveryv1.AddressType][]*discoveryv1.EndpointSlice)
 	errs := []error{}
@@ -119,7 +119,7 @@ func (r *endpointsReconciler) reconcile(svc *corev1.Service, ews []*ewv1alpha1.E
 // reconcileIPv4Endpoints operates on a set of external workloads, their
 // service, and any endpointslices that have been created by the controller. It
 // will compute the diff that needs to be written to the API Server.
-func (r *endpointsReconciler) reconcileByAddressType(svc *corev1.Service, extWorkloads []*ewv1alpha1.ExternalWorkload, existingSlices []*discoveryv1.EndpointSlice, addrType discoveryv1.AddressType) error {
+func (r *endpointsReconciler) reconcileByAddressType(svc *corev1.Service, extWorkloads []*ewv1beta1.ExternalWorkload, existingSlices []*discoveryv1.EndpointSlice, addrType discoveryv1.AddressType) error {
 	slicesToCreate := []*discoveryv1.EndpointSlice{}
 	slicesToUpdate := []*discoveryv1.EndpointSlice{}
 	slicesToDelete := []*discoveryv1.EndpointSlice{}
@@ -535,7 +535,7 @@ func setEndpointSliceLabels(es *discoveryv1.EndpointSlice, service *corev1.Servi
 	return svcLabels, updated
 }
 
-func externalWorkloadToEndpoint(addrType discoveryv1.AddressType, ew *ewv1alpha1.ExternalWorkload, svc *corev1.Service) discoveryv1.Endpoint {
+func externalWorkloadToEndpoint(addrType discoveryv1.AddressType, ew *ewv1beta1.ExternalWorkload, svc *corev1.Service) discoveryv1.Endpoint {
 	// Note: an ExternalWorkload does not have the same lifecycle as a pod; we
 	// do not mark a workload as "Terminating". Because of that, our code is
 	// simpler than the upstream and we never have to consider:
@@ -601,7 +601,7 @@ func ownedBy(slice *discoveryv1.EndpointSlice, svc *corev1.Service) bool {
 
 // findEndpointPorts is a utility function that will return a list of ports
 // that are documented on an external workload and selected by a service
-func (r *endpointsReconciler) findEndpointPorts(svc *corev1.Service, ew *ewv1alpha1.ExternalWorkload) []discoveryv1.EndpointPort {
+func (r *endpointsReconciler) findEndpointPorts(svc *corev1.Service, ew *ewv1beta1.ExternalWorkload) []discoveryv1.EndpointPort {
 	epPorts := []discoveryv1.EndpointPort{}
 	// If we are dealing with a headless service, upstream implementation allows
 	// the service not to have any ports
@@ -640,7 +640,7 @@ func (r *endpointsReconciler) findEndpointPorts(svc *corev1.Service, ew *ewv1alp
 // by the service.
 //
 // adapted from copied from k8s.io/kubernetes/pkg/api/v1/pod
-func findWorkloadPort(ew *ewv1alpha1.ExternalWorkload, svcPort *corev1.ServicePort) (int32, error) {
+func findWorkloadPort(ew *ewv1beta1.ExternalWorkload, svcPort *corev1.ServicePort) (int32, error) {
 	targetPort := svcPort.TargetPort
 	switch targetPort.Type {
 	case intstr.String:
diff --git a/controller/api/destination/watcher/endpoints_watcher.go b/controller/api/destination/watcher/endpoints_watcher.go
index 1dd638bbf..f7aa6fbb1 100644
--- a/controller/api/destination/watcher/endpoints_watcher.go
+++ b/controller/api/destination/watcher/endpoints_watcher.go
@@ -10,7 +10,7 @@ import (
 	"sync"
 	"time"
 
-	ewv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	ewv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta2"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	consts "github.com/linkerd/linkerd2/pkg/k8s"
@@ -49,7 +49,7 @@ type (
 		IP                string
 		Port              Port
 		Pod               *corev1.Pod
-		ExternalWorkload  *ewv1alpha1.ExternalWorkload
+		ExternalWorkload  *ewv1beta1.ExternalWorkload
 		OwnerName         string
 		OwnerKind         string
 		Identity          string
diff --git a/controller/api/destination/watcher/k8s.go b/controller/api/destination/watcher/k8s.go
index 5c4f03800..06a1a57a2 100644
--- a/controller/api/destination/watcher/k8s.go
+++ b/controller/api/destination/watcher/k8s.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"net"
 
-	ext "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	ext "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	"github.com/prometheus/client_golang/prometheus"
 	corev1 "k8s.io/api/core/v1"
diff --git a/controller/api/destination/watcher/workload_watcher.go b/controller/api/destination/watcher/workload_watcher.go
index a412fd420..2e1fd65d6 100644
--- a/controller/api/destination/watcher/workload_watcher.go
+++ b/controller/api/destination/watcher/workload_watcher.go
@@ -10,7 +10,7 @@ import (
 	"time"
 
 	externalworkload "github.com/linkerd/linkerd2/controller/api/destination/external-workload"
-	ext "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	ext "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta2"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	consts "github.com/linkerd/linkerd2/pkg/k8s"
diff --git a/controller/gen/apis/externalworkload/v1alpha1/doc.go b/controller/gen/apis/externalworkload/v1beta1/doc.go
similarity index 63%
rename from controller/gen/apis/externalworkload/v1alpha1/doc.go
rename to controller/gen/apis/externalworkload/v1beta1/doc.go
index 4601689c3..ffd023fe6 100644
--- a/controller/gen/apis/externalworkload/v1alpha1/doc.go
+++ b/controller/gen/apis/externalworkload/v1beta1/doc.go
@@ -1,3 +1,3 @@
 // +k8s:deepcopy-gen=package
 
-package v1alpha1
+package v1beta1
diff --git a/controller/gen/apis/externalworkload/v1alpha1/register.go b/controller/gen/apis/externalworkload/v1beta1/register.go
similarity index 97%
rename from controller/gen/apis/externalworkload/v1alpha1/register.go
rename to controller/gen/apis/externalworkload/v1beta1/register.go
index d47f6e9a3..1fe24a84e 100644
--- a/controller/gen/apis/externalworkload/v1alpha1/register.go
+++ b/controller/gen/apis/externalworkload/v1beta1/register.go
@@ -1,4 +1,4 @@
-package v1alpha1
+package v1beta1
 
 import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -13,7 +13,7 @@ var (
 	// of the group and the version of the API.
 	SchemeGroupVersion = schema.GroupVersion{
 		Group:   externalworkload.GroupName,
-		Version: "v1alpha1",
+		Version: "v1beta1",
 	}
 
 	// SchemeBuilder collects functions that add things to a scheme. It's to
diff --git a/controller/gen/apis/externalworkload/v1alpha1/types.go b/controller/gen/apis/externalworkload/v1beta1/types.go
similarity index 98%
rename from controller/gen/apis/externalworkload/v1alpha1/types.go
rename to controller/gen/apis/externalworkload/v1beta1/types.go
index 09ad65da2..3839b5c38 100644
--- a/controller/gen/apis/externalworkload/v1alpha1/types.go
+++ b/controller/gen/apis/externalworkload/v1beta1/types.go
@@ -1,4 +1,4 @@
-package v1alpha1
+package v1beta1
 
 import (
 	v1 "k8s.io/api/core/v1"
@@ -46,7 +46,7 @@ type ExternalWorkloadList struct {
 // ExternalWorkloadSpec represents the desired state of an external workload
 type ExternalWorkloadSpec struct {
 	// MeshTls describes TLS settings associated with an external workload
-	MeshTls MeshTls `json:"meshTls"`
+	MeshTLS MeshTLS `json:"meshTLS"`
 	// Ports describes a set of ports exposed by the workload
 	//
 	// +optional
@@ -59,7 +59,7 @@ type ExternalWorkloadSpec struct {
 }
 
 // MeshTls describes TLS settings associated with an external workload
-type MeshTls struct {
+type MeshTLS struct {
 	// Identity associated with the workload. Used by peers to perform
 	// verification in the mTLS handshake
 	Identity string `json:"identity"`
diff --git a/controller/gen/apis/externalworkload/v1alpha1/zz_generated.deepcopy.go b/controller/gen/apis/externalworkload/v1beta1/zz_generated.deepcopy.go
similarity index 96%
rename from controller/gen/apis/externalworkload/v1alpha1/zz_generated.deepcopy.go
rename to controller/gen/apis/externalworkload/v1beta1/zz_generated.deepcopy.go
index 0c312b71c..b3e60a13e 100644
--- a/controller/gen/apis/externalworkload/v1alpha1/zz_generated.deepcopy.go
+++ b/controller/gen/apis/externalworkload/v1beta1/zz_generated.deepcopy.go
@@ -19,7 +19,7 @@ limitations under the License.
 
 // Code generated by deepcopy-gen. DO NOT EDIT.
 
-package v1alpha1
+package v1beta1
 
 import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
@@ -89,7 +89,7 @@ func (in *ExternalWorkloadList) DeepCopyObject() runtime.Object {
 // 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
+	out.MeshTLS = in.MeshTLS
 	if in.Ports != nil {
 		in, out := &in.Ports, &out.Ports
 		*out = make([]PortSpec, len(*in))
@@ -137,17 +137,17 @@ func (in *ExternalWorkloadStatus) DeepCopy() *ExternalWorkloadStatus {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *MeshTls) DeepCopyInto(out *MeshTls) {
+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 {
+// 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)
+	out := new(MeshTLS)
 	in.DeepCopyInto(out)
 	return out
 }
diff --git a/controller/gen/client/clientset/versioned/clientset.go b/controller/gen/client/clientset/versioned/clientset.go
index 780e407eb..b02da86c7 100644
--- a/controller/gen/client/clientset/versioned/clientset.go
+++ b/controller/gen/client/clientset/versioned/clientset.go
@@ -22,7 +22,7 @@ import (
 	"fmt"
 	"net/http"
 
-	externalworkloadv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1"
+	externalworkloadv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1"
 	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"
@@ -36,7 +36,7 @@ import (
 
 type Interface interface {
 	Discovery() discovery.DiscoveryInterface
-	ExternalworkloadV1alpha1() externalworkloadv1alpha1.ExternalworkloadV1alpha1Interface
+	ExternalworkloadV1beta1() externalworkloadv1beta1.ExternalworkloadV1beta1Interface
 	LinkV1alpha1() linkv1alpha1.LinkV1alpha1Interface
 	PolicyV1alpha1() policyv1alpha1.PolicyV1alpha1Interface
 	PolicyV1beta3() policyv1beta3.PolicyV1beta3Interface
@@ -48,7 +48,7 @@ type Interface interface {
 // Clientset contains the clients for groups.
 type Clientset struct {
 	*discovery.DiscoveryClient
-	externalworkloadV1alpha1   *externalworkloadv1alpha1.ExternalworkloadV1alpha1Client
+	externalworkloadV1beta1    *externalworkloadv1beta1.ExternalworkloadV1beta1Client
 	linkV1alpha1               *linkv1alpha1.LinkV1alpha1Client
 	policyV1alpha1             *policyv1alpha1.PolicyV1alpha1Client
 	policyV1beta3              *policyv1beta3.PolicyV1beta3Client
@@ -57,9 +57,9 @@ type Clientset struct {
 	linkerdV1alpha2            *linkerdv1alpha2.LinkerdV1alpha2Client
 }
 
-// ExternalworkloadV1alpha1 retrieves the ExternalworkloadV1alpha1Client
-func (c *Clientset) ExternalworkloadV1alpha1() externalworkloadv1alpha1.ExternalworkloadV1alpha1Interface {
-	return c.externalworkloadV1alpha1
+// ExternalworkloadV1beta1 retrieves the ExternalworkloadV1beta1Client
+func (c *Clientset) ExternalworkloadV1beta1() externalworkloadv1beta1.ExternalworkloadV1beta1Interface {
+	return c.externalworkloadV1beta1
 }
 
 // LinkV1alpha1 retrieves the LinkV1alpha1Client
@@ -136,7 +136,7 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset,
 
 	var cs Clientset
 	var err error
-	cs.externalworkloadV1alpha1, err = externalworkloadv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient)
+	cs.externalworkloadV1beta1, err = externalworkloadv1beta1.NewForConfigAndClient(&configShallowCopy, httpClient)
 	if err != nil {
 		return nil, err
 	}
@@ -185,7 +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.externalworkloadV1beta1 = externalworkloadv1beta1.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 d74b11e6a..72a6c2637 100644
--- a/controller/gen/client/clientset/versioned/fake/clientset_generated.go
+++ b/controller/gen/client/clientset/versioned/fake/clientset_generated.go
@@ -20,8 +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"
+	externalworkloadv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1"
+	fakeexternalworkloadv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/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"
@@ -91,9 +91,9 @@ var (
 	_ testing.FakeClient  = &Clientset{}
 )
 
-// ExternalworkloadV1alpha1 retrieves the ExternalworkloadV1alpha1Client
-func (c *Clientset) ExternalworkloadV1alpha1() externalworkloadv1alpha1.ExternalworkloadV1alpha1Interface {
-	return &fakeexternalworkloadv1alpha1.FakeExternalworkloadV1alpha1{Fake: &c.Fake}
+// ExternalworkloadV1beta1 retrieves the ExternalworkloadV1beta1Client
+func (c *Clientset) ExternalworkloadV1beta1() externalworkloadv1beta1.ExternalworkloadV1beta1Interface {
+	return &fakeexternalworkloadv1beta1.FakeExternalworkloadV1beta1{Fake: &c.Fake}
 }
 
 // LinkV1alpha1 retrieves the LinkV1alpha1Client
diff --git a/controller/gen/client/clientset/versioned/fake/register.go b/controller/gen/client/clientset/versioned/fake/register.go
index a9ad313d5..c4aa0e327 100644
--- a/controller/gen/client/clientset/versioned/fake/register.go
+++ b/controller/gen/client/clientset/versioned/fake/register.go
@@ -19,7 +19,7 @@ limitations under the License.
 package fake
 
 import (
-	externalworkloadv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	externalworkloadv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	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"
@@ -37,7 +37,7 @@ var scheme = runtime.NewScheme()
 var codecs = serializer.NewCodecFactory(scheme)
 
 var localSchemeBuilder = runtime.SchemeBuilder{
-	externalworkloadv1alpha1.AddToScheme,
+	externalworkloadv1beta1.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 6e1d9af68..192576aa8 100644
--- a/controller/gen/client/clientset/versioned/scheme/register.go
+++ b/controller/gen/client/clientset/versioned/scheme/register.go
@@ -19,7 +19,7 @@ limitations under the License.
 package scheme
 
 import (
-	externalworkloadv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	externalworkloadv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	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"
@@ -37,7 +37,7 @@ var Scheme = runtime.NewScheme()
 var Codecs = serializer.NewCodecFactory(Scheme)
 var ParameterCodec = runtime.NewParameterCodec(Scheme)
 var localSchemeBuilder = runtime.SchemeBuilder{
-	externalworkloadv1alpha1.AddToScheme,
+	externalworkloadv1beta1.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/v1beta1/doc.go
similarity index 97%
rename from controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/doc.go
rename to controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/doc.go
index df51baa4d..771101956 100644
--- a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/doc.go
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/doc.go
@@ -17,4 +17,4 @@ limitations under the License.
 // Code generated by client-gen. DO NOT EDIT.
 
 // This package has the automatically generated typed clients.
-package v1alpha1
+package v1beta1
diff --git a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload.go b/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/externalworkload.go
similarity index 80%
rename from controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload.go
rename to controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/externalworkload.go
index ac57766a6..74bdfe78d 100644
--- a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload.go
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/externalworkload.go
@@ -16,13 +16,13 @@ limitations under the License.
 
 // Code generated by client-gen. DO NOT EDIT.
 
-package v1alpha1
+package v1beta1
 
 import (
 	"context"
 	"time"
 
-	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	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"
@@ -38,14 +38,14 @@ type ExternalWorkloadsGetter interface {
 
 // 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)
+	Create(ctx context.Context, externalWorkload *v1beta1.ExternalWorkload, opts v1.CreateOptions) (*v1beta1.ExternalWorkload, error)
+	Update(ctx context.Context, externalWorkload *v1beta1.ExternalWorkload, opts v1.UpdateOptions) (*v1beta1.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)
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.ExternalWorkload, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1beta1.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)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.ExternalWorkload, err error)
 	ExternalWorkloadExpansion
 }
 
@@ -56,7 +56,7 @@ type externalWorkloads struct {
 }
 
 // newExternalWorkloads returns a ExternalWorkloads
-func newExternalWorkloads(c *ExternalworkloadV1alpha1Client, namespace string) *externalWorkloads {
+func newExternalWorkloads(c *ExternalworkloadV1beta1Client, namespace string) *externalWorkloads {
 	return &externalWorkloads{
 		client: c.RESTClient(),
 		ns:     namespace,
@@ -64,8 +64,8 @@ func newExternalWorkloads(c *ExternalworkloadV1alpha1Client, namespace string) *
 }
 
 // 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{}
+func (c *externalWorkloads) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.ExternalWorkload, err error) {
+	result = &v1beta1.ExternalWorkload{}
 	err = c.client.Get().
 		Namespace(c.ns).
 		Resource("externalworkloads").
@@ -77,12 +77,12 @@ func (c *externalWorkloads) Get(ctx context.Context, name string, options v1.Get
 }
 
 // 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) {
+func (c *externalWorkloads) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.ExternalWorkloadList, err error) {
 	var timeout time.Duration
 	if opts.TimeoutSeconds != nil {
 		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
 	}
-	result = &v1alpha1.ExternalWorkloadList{}
+	result = &v1beta1.ExternalWorkloadList{}
 	err = c.client.Get().
 		Namespace(c.ns).
 		Resource("externalworkloads").
@@ -109,8 +109,8 @@ func (c *externalWorkloads) Watch(ctx context.Context, opts v1.ListOptions) (wat
 }
 
 // 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{}
+func (c *externalWorkloads) Create(ctx context.Context, externalWorkload *v1beta1.ExternalWorkload, opts v1.CreateOptions) (result *v1beta1.ExternalWorkload, err error) {
+	result = &v1beta1.ExternalWorkload{}
 	err = c.client.Post().
 		Namespace(c.ns).
 		Resource("externalworkloads").
@@ -122,8 +122,8 @@ func (c *externalWorkloads) Create(ctx context.Context, externalWorkload *v1alph
 }
 
 // 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{}
+func (c *externalWorkloads) Update(ctx context.Context, externalWorkload *v1beta1.ExternalWorkload, opts v1.UpdateOptions) (result *v1beta1.ExternalWorkload, err error) {
+	result = &v1beta1.ExternalWorkload{}
 	err = c.client.Put().
 		Namespace(c.ns).
 		Resource("externalworkloads").
@@ -163,8 +163,8 @@ func (c *externalWorkloads) DeleteCollection(ctx context.Context, opts v1.Delete
 }
 
 // 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{}
+func (c *externalWorkloads) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.ExternalWorkload, err error) {
+	result = &v1beta1.ExternalWorkload{}
 	err = c.client.Patch(pt).
 		Namespace(c.ns).
 		Resource("externalworkloads").
diff --git a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload_client.go b/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/externalworkload_client.go
similarity index 63%
rename from controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload_client.go
rename to controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/externalworkload_client.go
index 0a58f2919..61175a841 100644
--- a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/externalworkload_client.go
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/externalworkload_client.go
@@ -16,34 +16,34 @@ limitations under the License.
 
 // Code generated by client-gen. DO NOT EDIT.
 
-package v1alpha1
+package v1beta1
 
 import (
 	"net/http"
 
-	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme"
 	rest "k8s.io/client-go/rest"
 )
 
-type ExternalworkloadV1alpha1Interface interface {
+type ExternalworkloadV1beta1Interface interface {
 	RESTClient() rest.Interface
 	ExternalWorkloadsGetter
 }
 
-// ExternalworkloadV1alpha1Client is used to interact with features provided by the externalworkload group.
-type ExternalworkloadV1alpha1Client struct {
+// ExternalworkloadV1beta1Client is used to interact with features provided by the externalworkload group.
+type ExternalworkloadV1beta1Client struct {
 	restClient rest.Interface
 }
 
-func (c *ExternalworkloadV1alpha1Client) ExternalWorkloads(namespace string) ExternalWorkloadInterface {
+func (c *ExternalworkloadV1beta1Client) ExternalWorkloads(namespace string) ExternalWorkloadInterface {
 	return newExternalWorkloads(c, namespace)
 }
 
-// NewForConfig creates a new ExternalworkloadV1alpha1Client for the given config.
+// NewForConfig creates a new ExternalworkloadV1beta1Client 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) {
+func NewForConfig(c *rest.Config) (*ExternalworkloadV1beta1Client, error) {
 	config := *c
 	if err := setConfigDefaults(&config); err != nil {
 		return nil, err
@@ -55,9 +55,9 @@ func NewForConfig(c *rest.Config) (*ExternalworkloadV1alpha1Client, error) {
 	return NewForConfigAndClient(&config, httpClient)
 }
 
-// NewForConfigAndClient creates a new ExternalworkloadV1alpha1Client for the given config and http client.
+// NewForConfigAndClient creates a new ExternalworkloadV1beta1Client 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) {
+func NewForConfigAndClient(c *rest.Config, h *http.Client) (*ExternalworkloadV1beta1Client, error) {
 	config := *c
 	if err := setConfigDefaults(&config); err != nil {
 		return nil, err
@@ -66,12 +66,12 @@ func NewForConfigAndClient(c *rest.Config, h *http.Client) (*ExternalworkloadV1a
 	if err != nil {
 		return nil, err
 	}
-	return &ExternalworkloadV1alpha1Client{client}, nil
+	return &ExternalworkloadV1beta1Client{client}, nil
 }
 
-// NewForConfigOrDie creates a new ExternalworkloadV1alpha1Client for the given config and
+// NewForConfigOrDie creates a new ExternalworkloadV1beta1Client for the given config and
 // panics if there is an error in the config.
-func NewForConfigOrDie(c *rest.Config) *ExternalworkloadV1alpha1Client {
+func NewForConfigOrDie(c *rest.Config) *ExternalworkloadV1beta1Client {
 	client, err := NewForConfig(c)
 	if err != nil {
 		panic(err)
@@ -79,13 +79,13 @@ func NewForConfigOrDie(c *rest.Config) *ExternalworkloadV1alpha1Client {
 	return client
 }
 
-// New creates a new ExternalworkloadV1alpha1Client for the given RESTClient.
-func New(c rest.Interface) *ExternalworkloadV1alpha1Client {
-	return &ExternalworkloadV1alpha1Client{c}
+// New creates a new ExternalworkloadV1beta1Client for the given RESTClient.
+func New(c rest.Interface) *ExternalworkloadV1beta1Client {
+	return &ExternalworkloadV1beta1Client{c}
 }
 
 func setConfigDefaults(config *rest.Config) error {
-	gv := v1alpha1.SchemeGroupVersion
+	gv := v1beta1.SchemeGroupVersion
 	config.GroupVersion = &gv
 	config.APIPath = "/apis"
 	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
@@ -99,7 +99,7 @@ func setConfigDefaults(config *rest.Config) error {
 
 // RESTClient returns a RESTClient that is used to communicate
 // with API server by this client implementation.
-func (c *ExternalworkloadV1alpha1Client) RESTClient() rest.Interface {
+func (c *ExternalworkloadV1beta1Client) RESTClient() rest.Interface {
 	if c == nil {
 		return nil
 	}
diff --git a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/doc.go b/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/fake/doc.go
similarity index 100%
rename from controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/doc.go
rename to controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/fake/doc.go
diff --git a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/fake_externalworkload.go b/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/fake/fake_externalworkload.go
similarity index 71%
rename from controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/fake_externalworkload.go
rename to controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/fake/fake_externalworkload.go
index 7d5212df5..7a8562456 100644
--- a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/fake_externalworkload.go
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/fake/fake_externalworkload.go
@@ -21,7 +21,7 @@ package fake
 import (
 	"context"
 
-	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	labels "k8s.io/apimachinery/pkg/labels"
 	types "k8s.io/apimachinery/pkg/types"
@@ -31,29 +31,29 @@ import (
 
 // FakeExternalWorkloads implements ExternalWorkloadInterface
 type FakeExternalWorkloads struct {
-	Fake *FakeExternalworkloadV1alpha1
+	Fake *FakeExternalworkloadV1beta1
 	ns   string
 }
 
-var externalworkloadsResource = v1alpha1.SchemeGroupVersion.WithResource("externalworkloads")
+var externalworkloadsResource = v1beta1.SchemeGroupVersion.WithResource("externalworkloads")
 
-var externalworkloadsKind = v1alpha1.SchemeGroupVersion.WithKind("ExternalWorkload")
+var externalworkloadsKind = v1beta1.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) {
+func (c *FakeExternalWorkloads) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.ExternalWorkload, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewGetAction(externalworkloadsResource, c.ns, name), &v1alpha1.ExternalWorkload{})
+		Invokes(testing.NewGetAction(externalworkloadsResource, c.ns, name), &v1beta1.ExternalWorkload{})
 
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.ExternalWorkload), err
+	return obj.(*v1beta1.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) {
+func (c *FakeExternalWorkloads) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.ExternalWorkloadList, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewListAction(externalworkloadsResource, externalworkloadsKind, c.ns, opts), &v1alpha1.ExternalWorkloadList{})
+		Invokes(testing.NewListAction(externalworkloadsResource, externalworkloadsKind, c.ns, opts), &v1beta1.ExternalWorkloadList{})
 
 	if obj == nil {
 		return nil, err
@@ -63,8 +63,8 @@ func (c *FakeExternalWorkloads) List(ctx context.Context, opts v1.ListOptions) (
 	if label == nil {
 		label = labels.Everything()
 	}
-	list := &v1alpha1.ExternalWorkloadList{ListMeta: obj.(*v1alpha1.ExternalWorkloadList).ListMeta}
-	for _, item := range obj.(*v1alpha1.ExternalWorkloadList).Items {
+	list := &v1beta1.ExternalWorkloadList{ListMeta: obj.(*v1beta1.ExternalWorkloadList).ListMeta}
+	for _, item := range obj.(*v1beta1.ExternalWorkloadList).Items {
 		if label.Matches(labels.Set(item.Labels)) {
 			list.Items = append(list.Items, item)
 		}
@@ -80,31 +80,31 @@ func (c *FakeExternalWorkloads) Watch(ctx context.Context, opts v1.ListOptions)
 }
 
 // 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) {
+func (c *FakeExternalWorkloads) Create(ctx context.Context, externalWorkload *v1beta1.ExternalWorkload, opts v1.CreateOptions) (result *v1beta1.ExternalWorkload, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewCreateAction(externalworkloadsResource, c.ns, externalWorkload), &v1alpha1.ExternalWorkload{})
+		Invokes(testing.NewCreateAction(externalworkloadsResource, c.ns, externalWorkload), &v1beta1.ExternalWorkload{})
 
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.ExternalWorkload), err
+	return obj.(*v1beta1.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) {
+func (c *FakeExternalWorkloads) Update(ctx context.Context, externalWorkload *v1beta1.ExternalWorkload, opts v1.UpdateOptions) (result *v1beta1.ExternalWorkload, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewUpdateAction(externalworkloadsResource, c.ns, externalWorkload), &v1alpha1.ExternalWorkload{})
+		Invokes(testing.NewUpdateAction(externalworkloadsResource, c.ns, externalWorkload), &v1beta1.ExternalWorkload{})
 
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.ExternalWorkload), err
+	return obj.(*v1beta1.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{})
+		Invokes(testing.NewDeleteActionWithOptions(externalworkloadsResource, c.ns, name, opts), &v1beta1.ExternalWorkload{})
 
 	return err
 }
@@ -113,17 +113,17 @@ func (c *FakeExternalWorkloads) Delete(ctx context.Context, name string, opts v1
 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{})
+	_, err := c.Fake.Invokes(action, &v1beta1.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) {
+func (c *FakeExternalWorkloads) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.ExternalWorkload, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewPatchSubresourceAction(externalworkloadsResource, c.ns, name, pt, data, subresources...), &v1alpha1.ExternalWorkload{})
+		Invokes(testing.NewPatchSubresourceAction(externalworkloadsResource, c.ns, name, pt, data, subresources...), &v1beta1.ExternalWorkload{})
 
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.ExternalWorkload), err
+	return obj.(*v1beta1.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/v1beta1/fake/fake_externalworkload_client.go
similarity index 73%
rename from controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/fake_externalworkload_client.go
rename to controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/fake/fake_externalworkload_client.go
index b0c3ba2f9..3fe10e1a4 100644
--- a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/fake/fake_externalworkload_client.go
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/fake/fake_externalworkload_client.go
@@ -19,22 +19,22 @@ limitations under the License.
 package fake
 
 import (
-	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1"
 	rest "k8s.io/client-go/rest"
 	testing "k8s.io/client-go/testing"
 )
 
-type FakeExternalworkloadV1alpha1 struct {
+type FakeExternalworkloadV1beta1 struct {
 	*testing.Fake
 }
 
-func (c *FakeExternalworkloadV1alpha1) ExternalWorkloads(namespace string) v1alpha1.ExternalWorkloadInterface {
+func (c *FakeExternalworkloadV1beta1) ExternalWorkloads(namespace string) v1beta1.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 {
+func (c *FakeExternalworkloadV1beta1) 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/v1beta1/generated_expansion.go
similarity index 97%
rename from controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/generated_expansion.go
rename to controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/generated_expansion.go
index cbd85d00b..1acc7c16b 100644
--- a/controller/gen/client/clientset/versioned/typed/externalworkload/v1alpha1/generated_expansion.go
+++ b/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/generated_expansion.go
@@ -16,6 +16,6 @@ limitations under the License.
 
 // Code generated by client-gen. DO NOT EDIT.
 
-package v1alpha1
+package v1beta1
 
 type ExternalWorkloadExpansion interface{}
diff --git a/controller/gen/client/informers/externalversions/externalworkload/interface.go b/controller/gen/client/informers/externalversions/externalworkload/interface.go
index b4a55f4e7..5baa0720a 100644
--- a/controller/gen/client/informers/externalversions/externalworkload/interface.go
+++ b/controller/gen/client/informers/externalversions/externalworkload/interface.go
@@ -19,14 +19,14 @@ limitations under the License.
 package externalworkload
 
 import (
-	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/externalworkload/v1alpha1"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/externalworkload/v1beta1"
 	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
+	// V1beta1 provides access to shared informers for resources in V1beta1.
+	V1beta1() v1beta1.Interface
 }
 
 type group struct {
@@ -40,7 +40,7 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList
 	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)
+// V1beta1 returns a new v1beta1.Interface.
+func (g *group) V1beta1() v1beta1.Interface {
+	return v1beta1.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/v1beta1/externalworkload.go
similarity index 80%
rename from controller/gen/client/informers/externalversions/externalworkload/v1alpha1/externalworkload.go
rename to controller/gen/client/informers/externalversions/externalworkload/v1beta1/externalworkload.go
index 3e748b2ca..9eca2f243 100644
--- a/controller/gen/client/informers/externalversions/externalworkload/v1alpha1/externalworkload.go
+++ b/controller/gen/client/informers/externalversions/externalworkload/v1beta1/externalworkload.go
@@ -16,16 +16,16 @@ limitations under the License.
 
 // Code generated by informer-gen. DO NOT EDIT.
 
-package v1alpha1
+package v1beta1
 
 import (
 	"context"
 	time "time"
 
-	externalworkloadv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	externalworkloadv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	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"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/client/listers/externalworkload/v1beta1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 	watch "k8s.io/apimachinery/pkg/watch"
@@ -36,7 +36,7 @@ import (
 // ExternalWorkloads.
 type ExternalWorkloadInformer interface {
 	Informer() cache.SharedIndexInformer
-	Lister() v1alpha1.ExternalWorkloadLister
+	Lister() v1beta1.ExternalWorkloadLister
 }
 
 type externalWorkloadInformer struct {
@@ -62,16 +62,16 @@ func NewFilteredExternalWorkloadInformer(client versioned.Interface, namespace s
 				if tweakListOptions != nil {
 					tweakListOptions(&options)
 				}
-				return client.ExternalworkloadV1alpha1().ExternalWorkloads(namespace).List(context.TODO(), options)
+				return client.ExternalworkloadV1beta1().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)
+				return client.ExternalworkloadV1beta1().ExternalWorkloads(namespace).Watch(context.TODO(), options)
 			},
 		},
-		&externalworkloadv1alpha1.ExternalWorkload{},
+		&externalworkloadv1beta1.ExternalWorkload{},
 		resyncPeriod,
 		indexers,
 	)
@@ -82,9 +82,9 @@ func (f *externalWorkloadInformer) defaultInformer(client versioned.Interface, r
 }
 
 func (f *externalWorkloadInformer) Informer() cache.SharedIndexInformer {
-	return f.factory.InformerFor(&externalworkloadv1alpha1.ExternalWorkload{}, f.defaultInformer)
+	return f.factory.InformerFor(&externalworkloadv1beta1.ExternalWorkload{}, f.defaultInformer)
 }
 
-func (f *externalWorkloadInformer) Lister() v1alpha1.ExternalWorkloadLister {
-	return v1alpha1.NewExternalWorkloadLister(f.Informer().GetIndexer())
+func (f *externalWorkloadInformer) Lister() v1beta1.ExternalWorkloadLister {
+	return v1beta1.NewExternalWorkloadLister(f.Informer().GetIndexer())
 }
diff --git a/controller/gen/client/informers/externalversions/externalworkload/v1alpha1/interface.go b/controller/gen/client/informers/externalversions/externalworkload/v1beta1/interface.go
similarity index 98%
rename from controller/gen/client/informers/externalversions/externalworkload/v1alpha1/interface.go
rename to controller/gen/client/informers/externalversions/externalworkload/v1beta1/interface.go
index 1866f95de..dda490b91 100644
--- a/controller/gen/client/informers/externalversions/externalworkload/v1alpha1/interface.go
+++ b/controller/gen/client/informers/externalversions/externalworkload/v1beta1/interface.go
@@ -16,7 +16,7 @@ limitations under the License.
 
 // Code generated by informer-gen. DO NOT EDIT.
 
-package v1alpha1
+package v1beta1
 
 import (
 	internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces"
diff --git a/controller/gen/client/informers/externalversions/generic.go b/controller/gen/client/informers/externalversions/generic.go
index 4a55a9f3d..d49fbc0fe 100644
--- a/controller/gen/client/informers/externalversions/generic.go
+++ b/controller/gen/client/informers/externalversions/generic.go
@@ -21,12 +21,12 @@ package externalversions
 import (
 	"fmt"
 
-	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
-	linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha1"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
+	v1alpha1 "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"
 	v1beta2 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta2"
-	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
+	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
 	v1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
 	schema "k8s.io/apimachinery/pkg/runtime/schema"
 	cache "k8s.io/client-go/tools/cache"
@@ -58,12 +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=externalworkload, Version=v1alpha1
-	case v1alpha1.SchemeGroupVersion.WithResource("externalworkloads"):
-		return &genericInformer{resource: resource.GroupResource(), informer: f.Externalworkload().V1alpha1().ExternalWorkloads().Informer()}, nil
+	// Group=externalworkload, Version=v1beta1
+	case v1beta1.SchemeGroupVersion.WithResource("externalworkloads"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Externalworkload().V1beta1().ExternalWorkloads().Informer()}, nil
 
 		// Group=link, Version=v1alpha1
-	case linkv1alpha1.SchemeGroupVersion.WithResource("links"):
+	case v1alpha1.SchemeGroupVersion.WithResource("links"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Link().V1alpha1().Links().Informer()}, nil
 
 		// Group=linkerd.io, Version=v1alpha2
@@ -89,7 +89,7 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Server().V1beta2().Servers().Informer()}, nil
 
 		// Group=serverauthorization, Version=v1beta1
-	case v1beta1.SchemeGroupVersion.WithResource("serverauthorizations"):
+	case serverauthorizationv1beta1.SchemeGroupVersion.WithResource("serverauthorizations"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Serverauthorization().V1beta1().ServerAuthorizations().Informer()}, nil
 
 	}
diff --git a/controller/gen/client/listers/externalworkload/v1alpha1/expansion_generated.go b/controller/gen/client/listers/externalworkload/v1beta1/expansion_generated.go
similarity index 98%
rename from controller/gen/client/listers/externalworkload/v1alpha1/expansion_generated.go
rename to controller/gen/client/listers/externalworkload/v1beta1/expansion_generated.go
index 5c77b2b63..567a912a2 100644
--- a/controller/gen/client/listers/externalworkload/v1alpha1/expansion_generated.go
+++ b/controller/gen/client/listers/externalworkload/v1beta1/expansion_generated.go
@@ -16,7 +16,7 @@ limitations under the License.
 
 // Code generated by lister-gen. DO NOT EDIT.
 
-package v1alpha1
+package v1beta1
 
 // ExternalWorkloadListerExpansion allows custom methods to be added to
 // ExternalWorkloadLister.
diff --git a/controller/gen/client/listers/externalworkload/v1alpha1/externalworkload.go b/controller/gen/client/listers/externalworkload/v1beta1/externalworkload.go
similarity index 80%
rename from controller/gen/client/listers/externalworkload/v1alpha1/externalworkload.go
rename to controller/gen/client/listers/externalworkload/v1beta1/externalworkload.go
index 7b39381af..f3c860077 100644
--- a/controller/gen/client/listers/externalworkload/v1alpha1/externalworkload.go
+++ b/controller/gen/client/listers/externalworkload/v1beta1/externalworkload.go
@@ -16,10 +16,10 @@ limitations under the License.
 
 // Code generated by lister-gen. DO NOT EDIT.
 
-package v1alpha1
+package v1beta1
 
 import (
-	v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/client-go/tools/cache"
@@ -30,7 +30,7 @@ import (
 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)
+	List(selector labels.Selector) (ret []*v1beta1.ExternalWorkload, err error)
 	// ExternalWorkloads returns an object that can list and get ExternalWorkloads.
 	ExternalWorkloads(namespace string) ExternalWorkloadNamespaceLister
 	ExternalWorkloadListerExpansion
@@ -47,9 +47,9 @@ func NewExternalWorkloadLister(indexer cache.Indexer) ExternalWorkloadLister {
 }
 
 // List lists all ExternalWorkloads in the indexer.
-func (s *externalWorkloadLister) List(selector labels.Selector) (ret []*v1alpha1.ExternalWorkload, err error) {
+func (s *externalWorkloadLister) List(selector labels.Selector) (ret []*v1beta1.ExternalWorkload, err error) {
 	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
-		ret = append(ret, m.(*v1alpha1.ExternalWorkload))
+		ret = append(ret, m.(*v1beta1.ExternalWorkload))
 	})
 	return ret, err
 }
@@ -64,10 +64,10 @@ func (s *externalWorkloadLister) ExternalWorkloads(namespace string) ExternalWor
 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)
+	List(selector labels.Selector) (ret []*v1beta1.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)
+	Get(name string) (*v1beta1.ExternalWorkload, error)
 	ExternalWorkloadNamespaceListerExpansion
 }
 
@@ -79,21 +79,21 @@ type externalWorkloadNamespaceLister struct {
 }
 
 // List lists all ExternalWorkloads in the indexer for a given namespace.
-func (s externalWorkloadNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.ExternalWorkload, err error) {
+func (s externalWorkloadNamespaceLister) List(selector labels.Selector) (ret []*v1beta1.ExternalWorkload, err error) {
 	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
-		ret = append(ret, m.(*v1alpha1.ExternalWorkload))
+		ret = append(ret, m.(*v1beta1.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) {
+func (s externalWorkloadNamespaceLister) Get(name string) (*v1beta1.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 nil, errors.NewNotFound(v1beta1.Resource("externalworkload"), name)
 	}
-	return obj.(*v1alpha1.ExternalWorkload), nil
+	return obj.(*v1beta1.ExternalWorkload), nil
 }
diff --git a/controller/k8s/api.go b/controller/k8s/api.go
index fb23b81f6..f6be6e24c 100644
--- a/controller/k8s/api.go
+++ b/controller/k8s/api.go
@@ -11,7 +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"
+	ewinformers "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/externalworkload/v1beta1"
 	srvinformers "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/server/v1beta2"
 	spinformers "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/serviceprofile/v1alpha2"
 	"github.com/linkerd/linkerd2/pkg/k8s"
@@ -241,7 +241,7 @@ func newAPI(
 			if l5dCrdSharedInformers == nil {
 				panic("Linkerd CRD shared informer not configured")
 			}
-			api.ew = l5dCrdSharedInformers.Externalworkload().V1alpha1().ExternalWorkloads()
+			api.ew = l5dCrdSharedInformers.Externalworkload().V1beta1().ExternalWorkloads()
 			api.syncChecks = append(api.syncChecks, api.ew.Informer().HasSynced)
 			api.promGauges.addInformerSize(k8s.ExtWorkload, informerLabels, api.ew.Informer())
 		case Job:
diff --git a/jaeger/charts/linkerd-jaeger/Chart.yaml b/jaeger/charts/linkerd-jaeger/Chart.yaml
index a6b6356d4..e0531e2f5 100644
--- a/jaeger/charts/linkerd-jaeger/Chart.yaml
+++ b/jaeger/charts/linkerd-jaeger/Chart.yaml
@@ -11,7 +11,7 @@ kubeVersion: ">=1.22.0-0"
 name: linkerd-jaeger
 sources:
   - https://github.com/linkerd/linkerd2/
-version: 30.14.8-edge
+version: 30.14.9-edge
 icon: https://linkerd.io/images/logo-only-200h.png
 maintainers:
   - name: Linkerd authors
diff --git a/jaeger/charts/linkerd-jaeger/README.md b/jaeger/charts/linkerd-jaeger/README.md
index 17fde5e92..e83c334e6 100644
--- a/jaeger/charts/linkerd-jaeger/README.md
+++ b/jaeger/charts/linkerd-jaeger/README.md
@@ -3,7 +3,7 @@
 The Linkerd-Jaeger extension adds distributed tracing to Linkerd using
 OpenCensus and Jaeger.
 
-![Version: 30.14.8-edge](https://img.shields.io/badge/Version-30.14.8--edge-informational?style=flat-square)
+![Version: 30.14.9-edge](https://img.shields.io/badge/Version-30.14.9--edge-informational?style=flat-square)
 
 ![AppVersion: edge-XX.X.X](https://img.shields.io/badge/AppVersion-edge--XX.X.X-informational?style=flat-square)
 
diff --git a/multicluster/charts/linkerd-multicluster/Chart.yaml b/multicluster/charts/linkerd-multicluster/Chart.yaml
index ade642193..67d2b257b 100644
--- a/multicluster/charts/linkerd-multicluster/Chart.yaml
+++ b/multicluster/charts/linkerd-multicluster/Chart.yaml
@@ -11,7 +11,7 @@ kubeVersion: ">=1.22.0-0"
 name: "linkerd-multicluster"
 sources:
   - https://github.com/linkerd/linkerd2/
-version: 30.12.17-edge
+version: 30.12.18-edge
 icon: https://linkerd.io/images/logo-only-200h.png
 maintainers:
   - name: Linkerd authors
diff --git a/multicluster/charts/linkerd-multicluster/README.md b/multicluster/charts/linkerd-multicluster/README.md
index 53704b5f0..d9f2ec92a 100644
--- a/multicluster/charts/linkerd-multicluster/README.md
+++ b/multicluster/charts/linkerd-multicluster/README.md
@@ -3,7 +3,7 @@
 The Linkerd-Multicluster extension contains resources to support multicluster
 linking to remote clusters
 
-![Version: 30.12.17-edge](https://img.shields.io/badge/Version-30.12.17--edge-informational?style=flat-square)
+![Version: 30.12.18-edge](https://img.shields.io/badge/Version-30.12.18--edge-informational?style=flat-square)
 
 ![AppVersion: edge-XX.X.X](https://img.shields.io/badge/AppVersion-edge--XX.X.X-informational?style=flat-square)
 
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index 861b727f3..5b4a55261 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -8,7 +8,7 @@ package k8s
 import (
 	"fmt"
 
-	ewv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	ewv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"github.com/linkerd/linkerd2/pkg/version"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -515,7 +515,7 @@ func GetPodLabels(ownerKind, ownerName string, pod *corev1.Pod) map[string]strin
 }
 
 // GetExternalWorkloadLabels returns the set of prometheus owner labels for a given ExternalWorkload
-func GetExternalWorkloadLabels(ownerKind, ownerName string, ew *ewv1alpha1.ExternalWorkload) map[string]string {
+func GetExternalWorkloadLabels(ownerKind, ownerName string, ew *ewv1beta1.ExternalWorkload) map[string]string {
 	labels := map[string]string{"external_workload": ew.Name}
 
 	if ownerKind != "" && ownerName != "" {
diff --git a/policy-controller/k8s/api/src/external_workload.rs b/policy-controller/k8s/api/src/external_workload.rs
index 76f7fd0ed..f329e8440 100644
--- a/policy-controller/k8s/api/src/external_workload.rs
+++ b/policy-controller/k8s/api/src/external_workload.rs
@@ -9,14 +9,14 @@ use serde::{Deserialize, Serialize};
 #[derive(Clone, Debug, PartialEq, Eq, CustomResource, Deserialize, Serialize, JsonSchema)]
 #[kube(
     group = "workload.linkerd.io",
-    version = "v1alpha1",
+    version = "v1beta1",
     kind = "ExternalWorkload",
     status = "ExternalWorkloadStatus",
     namespaced
 )]
 pub struct ExternalWorkloadSpec {
     /// MeshTls describes TLS settings associated with an external workload
-    #[serde(rename = "meshTls")]
+    #[serde(rename = "meshTLS")]
     pub mesh_tls: MeshTls,
     /// Ports describes a set of ports exposed by the workload
     pub ports: Option<Vec<PortSpec>>,
diff --git a/policy-controller/k8s/index/src/inbound/index.rs b/policy-controller/k8s/index/src/inbound/index.rs
index d2861d0d2..2955a9004 100644
--- a/policy-controller/k8s/index/src/inbound/index.rs
+++ b/policy-controller/k8s/index/src/inbound/index.rs
@@ -397,6 +397,7 @@ impl kubert::index::IndexNamespacedResource<k8s::Pod> for Index {
             // watches will complete.  No other parts of the index need to be
             // updated.
             if ns.get_mut().pods.by_name.remove(&name).is_some() && ns.get().is_empty() {
+                tracing::debug!(namespace = ns.key(), "Removing empty namespace index");
                 ns.remove();
             }
         }
@@ -622,6 +623,7 @@ impl kubert::index::IndexNamespacedResource<k8s::policy::AuthorizationPolicy> fo
 
     fn delete(&mut self, ns: String, ap: String) {
         let _span = info_span!("delete", %ns, %ap).entered();
+        tracing::trace!(name = %ap, "Delete");
         self.ns_with_reindex(ns, |ns| {
             ns.policy.authorization_policies.remove(&ap).is_some()
         })
@@ -634,6 +636,7 @@ impl kubert::index::IndexNamespacedResource<k8s::policy::AuthorizationPolicy> fo
     ) {
         let _span = info_span!("reset");
 
+        tracing::trace!(?deleted, ?policies, "Reset");
         // Aggregate all of the updates by namespace so that we only reindex
         // once per namespace.
         type Ns = NsUpdate<String, authorization_policy::Spec>;
@@ -902,6 +905,7 @@ impl NamespaceIndex {
         if let Entry::Occupied(mut ns) = self.by_ns.entry(namespace) {
             if f(ns.get_mut()) {
                 if ns.get().is_empty() {
+                    tracing::debug!(namespace = ns.key(), "Removing empty namespace index");
                     ns.remove();
                 } else {
                     ns.get_mut().reindex(authns);
@@ -1454,7 +1458,10 @@ impl ExternalWorkload {
 impl PolicyIndex {
     #[inline]
     fn is_empty(&self) -> bool {
-        self.servers.is_empty() && self.server_authorizations.is_empty()
+        self.servers.is_empty()
+            && self.server_authorizations.is_empty()
+            && self.authorization_policies.is_empty()
+            && self.http_routes.is_empty()
     }
 
     fn update_server(&mut self, name: String, server: server::Server) -> bool {
diff --git a/viz/charts/linkerd-viz/Chart.yaml b/viz/charts/linkerd-viz/Chart.yaml
index b1803dbd1..4e3407a59 100644
--- a/viz/charts/linkerd-viz/Chart.yaml
+++ b/viz/charts/linkerd-viz/Chart.yaml
@@ -11,7 +11,7 @@ kubeVersion: ">=1.22.0-0"
 name: "linkerd-viz"
 sources:
   - https://github.com/linkerd/linkerd2/
-version: 30.14.8-edge
+version: 30.14.9-edge
 icon: https://linkerd.io/images/logo-only-200h.png
 maintainers:
   - name: Linkerd authors
diff --git a/viz/charts/linkerd-viz/README.md b/viz/charts/linkerd-viz/README.md
index 4cf081ad6..dd668fdad 100644
--- a/viz/charts/linkerd-viz/README.md
+++ b/viz/charts/linkerd-viz/README.md
@@ -3,7 +3,7 @@
 The Linkerd-Viz extension contains observability and visualization
 components for Linkerd.
 
-![Version: 30.14.8-edge](https://img.shields.io/badge/Version-30.14.8--edge-informational?style=flat-square)
+![Version: 30.14.9-edge](https://img.shields.io/badge/Version-30.14.9--edge-informational?style=flat-square)
 
 ![AppVersion: edge-XX.X.X](https://img.shields.io/badge/AppVersion-edge--XX.X.X-informational?style=flat-square)
 


Test patch:
diff --git a/bin/_test-helpers.sh b/bin/_test-helpers.sh
index 08a92054f..04f63f714 100644
--- a/bin/_test-helpers.sh
+++ b/bin/_test-helpers.sh
@@ -5,7 +5,7 @@
 set +e
 
 k8s_version_min='+v1.22'
-k8s_version_max='+v1.28'
+k8s_version_max='+v1.29'
 
 bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
 testdir="$bindir"/../test/integration
diff --git a/cli/cmd/testdata/install_crds.golden b/cli/cmd/testdata/install_crds.golden
index 7b252d821..3b68a30f4 100644
--- a/cli/cmd/testdata/install_crds.golden
+++ b/cli/cmd/testdata/install_crds.golden
@@ -10248,14 +10248,147 @@ spec:
     shortNames: []
   scope: Namespaced
   versions:
-  - additionalPrinterColumns:
+  - name: v1alpha1
+    served: true
+    storage: false
+    schema:
+      openAPIV3Schema:
+        description: >-
+          An ExternalWorkload describes a single workload (i.e. a deployable unit) external
+          to the cluster that should be enrolled in the mesh.
+        type: object
+        required: [spec]
+        properties:
+          apiVerson:
+            type: string
+          kind:
+            type: string
+          metadata:
+            type: object
+          spec:
+            properties:
+              meshTls:
+                description: meshTls describes TLS settings associated with an
+                  external workload.
+                properties:
+                  identity:
+                    type: string
+                    description: identity of the workload. Corresponds to the
+                      identity used in the workload's certificate. It is used
+                      by peers to perform verification in the mTLS handshake.
+                    minLength: 1
+                    maxLength: 253
+                  serverName:
+                    type: string
+                    description: serverName is the name of the workload in DNS
+                      format. It is used by the workload to terminate TLS using
+                      SNI.
+                    minLength: 1
+                    maxLength: 253
+                type: object
+                required:
+                - identity
+                - serverName
+              ports:
+                type: array
+                description: ports describes a list of ports exposed by the
+                  workload
+                items:
+                  properties:
+                    name:
+                      type: string
+                      description: name must be an IANA_SVC_NAME and unique
+                        within the ports set. Each named port can be referred
+                        to by services.
+                    port:
+                      format: int32
+                      maximum: 65535
+                      minimum: 1
+                      type: integer
+                    protocol:
+                      description: protocol exposed by the port. Must be UDP or
+                        TCP. Defaults to TCP.
+                      type: string
+                      default: "TCP"
+                  type: object
+                  required:
+                  - port
+              workloadIPs:
+                type: array
+                description: workloadIPs contains a list of IP addresses that
+                  can be used to send traffic to the workload.
+                items:
+                  type: object
+                  properties:
+                    ip:
+                      type: string
+                # TODO: relax this in the future when ipv6 is supported
+                # an external workload (like a pod) should only
+                # support 2 interfaces
+                maxItems: 1
+            type: object
+            required:
+            - meshTls
+          status:
+            type: object
+            properties:
+              conditions:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    lastProbeTime:
+                      description: lastProbeTime is the last time the
+                        healthcheck endpoint was probed.
+                      format: date-time
+                      type: string
+                    lastTransitionTime:
+                      description: lastTransitionTime is the last time the
+                        condition transitioned from one status to another.
+                      format: date-time
+                      type: string
+                    status:
+                      description: status of the condition (one of True, False, Unknown)
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of the condition in CamelCase or in
+                        foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                    reason:
+                      description: reason contains a programmatic identifier
+                        indicating the reason for the condition's last
+                        transition. Producers of specific condition types may
+                        define expected values and meanings for this field, and
+                        whether the values are considered a guaranteed API. The
+                        value should be a CamelCase string. This field may not
+                        be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    message:
+                      description: message is a human readable message
+                        indicating details about the transition. This may be an
+                        empty string.
+                      maxLength: 32768
+                      type: string
+                required:
+                - status
+                - type
+    additionalPrinterColumns:
     - jsonPath: .spec.meshTls.identity
       name: Identity
       type: string
     - jsonPath: .metadata.creationTimestamp
       name: Age
       type: date
-    name: v1alpha1
+  - name: v1beta1
     served: true
     storage: true
     schema:
@@ -10274,8 +10407,8 @@ spec:
             type: object
           spec:
             properties:
-              meshTls:
-                description: meshTls describes TLS settings associated with an
+              meshTLS:
+                description: meshTLS describes TLS settings associated with an
                   external workload.
                 properties:
                   identity:
@@ -10335,7 +10468,7 @@ spec:
                 maxItems: 1
             type: object
             required:
-            - meshTls
+            - meshTLS
           status:
             type: object
             properties:
@@ -10388,3 +10521,10 @@ spec:
                 required:
                 - status
                 - type
+    additionalPrinterColumns:
+    - jsonPath: .spec.meshTLS.identity
+      name: Identity
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
diff --git a/cli/cmd/testdata/install_helm_crds_output.golden b/cli/cmd/testdata/install_helm_crds_output.golden
index 65f3e16b3..2c07dc3d1 100644
--- a/cli/cmd/testdata/install_helm_crds_output.golden
+++ b/cli/cmd/testdata/install_helm_crds_output.golden
@@ -10266,14 +10266,147 @@ spec:
     shortNames: []
   scope: Namespaced
   versions:
-  - additionalPrinterColumns:
+  - name: v1alpha1
+    served: true
+    storage: false
+    schema:
+      openAPIV3Schema:
+        description: >-
+          An ExternalWorkload describes a single workload (i.e. a deployable unit) external
+          to the cluster that should be enrolled in the mesh.
+        type: object
+        required: [spec]
+        properties:
+          apiVerson:
+            type: string
+          kind:
+            type: string
+          metadata:
+            type: object
+          spec:
+            properties:
+              meshTls:
+                description: meshTls describes TLS settings associated with an
+                  external workload.
+                properties:
+                  identity:
+                    type: string
+                    description: identity of the workload. Corresponds to the
+                      identity used in the workload's certificate. It is used
+                      by peers to perform verification in the mTLS handshake.
+                    minLength: 1
+                    maxLength: 253
+                  serverName:
+                    type: string
+                    description: serverName is the name of the workload in DNS
+                      format. It is used by the workload to terminate TLS using
+                      SNI.
+                    minLength: 1
+                    maxLength: 253
+                type: object
+                required:
+                - identity
+                - serverName
+              ports:
+                type: array
+                description: ports describes a list of ports exposed by the
+                  workload
+                items:
+                  properties:
+                    name:
+                      type: string
+                      description: name must be an IANA_SVC_NAME and unique
+                        within the ports set. Each named port can be referred
+                        to by services.
+                    port:
+                      format: int32
+                      maximum: 65535
+                      minimum: 1
+                      type: integer
+                    protocol:
+                      description: protocol exposed by the port. Must be UDP or
+                        TCP. Defaults to TCP.
+                      type: string
+                      default: "TCP"
+                  type: object
+                  required:
+                  - port
+              workloadIPs:
+                type: array
+                description: workloadIPs contains a list of IP addresses that
+                  can be used to send traffic to the workload.
+                items:
+                  type: object
+                  properties:
+                    ip:
+                      type: string
+                # TODO: relax this in the future when ipv6 is supported
+                # an external workload (like a pod) should only
+                # support 2 interfaces
+                maxItems: 1
+            type: object
+            required:
+            - meshTls
+          status:
+            type: object
+            properties:
+              conditions:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    lastProbeTime:
+                      description: lastProbeTime is the last time the
+                        healthcheck endpoint was probed.
+                      format: date-time
+                      type: string
+                    lastTransitionTime:
+                      description: lastTransitionTime is the last time the
+                        condition transitioned from one status to another.
+                      format: date-time
+                      type: string
+                    status:
+                      description: status of the condition (one of True, False, Unknown)
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of the condition in CamelCase or in
+                        foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                    reason:
+                      description: reason contains a programmatic identifier
+                        indicating the reason for the condition's last
+                        transition. Producers of specific condition types may
+                        define expected values and meanings for this field, and
+                        whether the values are considered a guaranteed API. The
+                        value should be a CamelCase string. This field may not
+                        be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    message:
+                      description: message is a human readable message
+                        indicating details about the transition. This may be an
+                        empty string.
+                      maxLength: 32768
+                      type: string
+                required:
+                - status
+                - type
+    additionalPrinterColumns:
     - jsonPath: .spec.meshTls.identity
       name: Identity
       type: string
     - jsonPath: .metadata.creationTimestamp
       name: Age
       type: date
-    name: v1alpha1
+  - name: v1beta1
     served: true
     storage: true
     schema:
@@ -10292,8 +10425,8 @@ spec:
             type: object
           spec:
             properties:
-              meshTls:
-                description: meshTls describes TLS settings associated with an
+              meshTLS:
+                description: meshTLS describes TLS settings associated with an
                   external workload.
                 properties:
                   identity:
@@ -10353,7 +10486,7 @@ spec:
                 maxItems: 1
             type: object
             required:
-            - meshTls
+            - meshTLS
           status:
             type: object
             properties:
@@ -10406,3 +10539,10 @@ spec:
                 required:
                 - status
                 - type
+    additionalPrinterColumns:
+    - jsonPath: .spec.meshTLS.identity
+      name: Identity
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
diff --git a/cli/cmd/testdata/install_helm_crds_output_ha.golden b/cli/cmd/testdata/install_helm_crds_output_ha.golden
index 65f3e16b3..2c07dc3d1 100644
--- a/cli/cmd/testdata/install_helm_crds_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_crds_output_ha.golden
@@ -10266,14 +10266,147 @@ spec:
     shortNames: []
   scope: Namespaced
   versions:
-  - additionalPrinterColumns:
+  - name: v1alpha1
+    served: true
+    storage: false
+    schema:
+      openAPIV3Schema:
+        description: >-
+          An ExternalWorkload describes a single workload (i.e. a deployable unit) external
+          to the cluster that should be enrolled in the mesh.
+        type: object
+        required: [spec]
+        properties:
+          apiVerson:
+            type: string
+          kind:
+            type: string
+          metadata:
+            type: object
+          spec:
+            properties:
+              meshTls:
+                description: meshTls describes TLS settings associated with an
+                  external workload.
+                properties:
+                  identity:
+                    type: string
+                    description: identity of the workload. Corresponds to the
+                      identity used in the workload's certificate. It is used
+                      by peers to perform verification in the mTLS handshake.
+                    minLength: 1
+                    maxLength: 253
+                  serverName:
+                    type: string
+                    description: serverName is the name of the workload in DNS
+                      format. It is used by the workload to terminate TLS using
+                      SNI.
+                    minLength: 1
+                    maxLength: 253
+                type: object
+                required:
+                - identity
+                - serverName
+              ports:
+                type: array
+                description: ports describes a list of ports exposed by the
+                  workload
+                items:
+                  properties:
+                    name:
+                      type: string
+                      description: name must be an IANA_SVC_NAME and unique
+                        within the ports set. Each named port can be referred
+                        to by services.
+                    port:
+                      format: int32
+                      maximum: 65535
+                      minimum: 1
+                      type: integer
+                    protocol:
+                      description: protocol exposed by the port. Must be UDP or
+                        TCP. Defaults to TCP.
+                      type: string
+                      default: "TCP"
+                  type: object
+                  required:
+                  - port
+              workloadIPs:
+                type: array
+                description: workloadIPs contains a list of IP addresses that
+                  can be used to send traffic to the workload.
+                items:
+                  type: object
+                  properties:
+                    ip:
+                      type: string
+                # TODO: relax this in the future when ipv6 is supported
+                # an external workload (like a pod) should only
+                # support 2 interfaces
+                maxItems: 1
+            type: object
+            required:
+            - meshTls
+          status:
+            type: object
+            properties:
+              conditions:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    lastProbeTime:
+                      description: lastProbeTime is the last time the
+                        healthcheck endpoint was probed.
+                      format: date-time
+                      type: string
+                    lastTransitionTime:
+                      description: lastTransitionTime is the last time the
+                        condition transitioned from one status to another.
+                      format: date-time
+                      type: string
+                    status:
+                      description: status of the condition (one of True, False, Unknown)
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of the condition in CamelCase or in
+                        foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                    reason:
+                      description: reason contains a programmatic identifier
+                        indicating the reason for the condition's last
+                        transition. Producers of specific condition types may
+                        define expected values and meanings for this field, and
+                        whether the values are considered a guaranteed API. The
+                        value should be a CamelCase string. This field may not
+                        be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    message:
+                      description: message is a human readable message
+                        indicating details about the transition. This may be an
+                        empty string.
+                      maxLength: 32768
+                      type: string
+                required:
+                - status
+                - type
+    additionalPrinterColumns:
     - jsonPath: .spec.meshTls.identity
       name: Identity
       type: string
     - jsonPath: .metadata.creationTimestamp
       name: Age
       type: date
-    name: v1alpha1
+  - name: v1beta1
     served: true
     storage: true
     schema:
@@ -10292,8 +10425,8 @@ spec:
             type: object
           spec:
             properties:
-              meshTls:
-                description: meshTls describes TLS settings associated with an
+              meshTLS:
+                description: meshTLS describes TLS settings associated with an
                   external workload.
                 properties:
                   identity:
@@ -10353,7 +10486,7 @@ spec:
                 maxItems: 1
             type: object
             required:
-            - meshTls
+            - meshTLS
           status:
             type: object
             properties:
@@ -10406,3 +10539,10 @@ spec:
                 required:
                 - status
                 - type
+    additionalPrinterColumns:
+    - jsonPath: .spec.meshTLS.identity
+      name: Identity
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
diff --git a/controller/api/destination/endpoint_translator_test.go b/controller/api/destination/endpoint_translator_test.go
index 6c4759c9c..fb28e010a 100644
--- a/controller/api/destination/endpoint_translator_test.go
+++ b/controller/api/destination/endpoint_translator_test.go
@@ -11,7 +11,7 @@ import (
 	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
 	"github.com/linkerd/linkerd2-proxy-api/go/net"
 	"github.com/linkerd/linkerd2/controller/api/destination/watcher"
-	ewv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	ewv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"github.com/linkerd/linkerd2/pkg/addr"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	corev1 "k8s.io/api/core/v1"
@@ -105,13 +105,13 @@ var (
 	ew1 = watcher.Address{
 		IP:   "1.1.1.1",
 		Port: 1,
-		ExternalWorkload: &ewv1alpha1.ExternalWorkload{
+		ExternalWorkload: &ewv1beta1.ExternalWorkload{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "ew-1",
 				Namespace: "ns",
 			},
-			Spec: ewv1alpha1.ExternalWorkloadSpec{
-				MeshTls: ewv1alpha1.MeshTls{
+			Spec: ewv1beta1.ExternalWorkloadSpec{
+				MeshTLS: ewv1beta1.MeshTLS{
 					Identity:   "spiffe://some-domain/ew-1",
 					ServerName: "server.local",
 				},
@@ -124,7 +124,7 @@ var (
 	ew2 = watcher.Address{
 		IP:   "1.1.1.2",
 		Port: 2,
-		ExternalWorkload: &ewv1alpha1.ExternalWorkload{
+		ExternalWorkload: &ewv1beta1.ExternalWorkload{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "ew-2",
 				Namespace: "ns",
@@ -133,8 +133,8 @@ var (
 					k8s.ProxyDeploymentLabel: "deployment-name",
 				},
 			},
-			Spec: ewv1alpha1.ExternalWorkloadSpec{
-				MeshTls: ewv1alpha1.MeshTls{
+			Spec: ewv1beta1.ExternalWorkloadSpec{
+				MeshTLS: ewv1beta1.MeshTLS{
 					Identity:   "spiffe://some-domain/ew-2",
 					ServerName: "server.local",
 				},
@@ -145,7 +145,7 @@ var (
 	ew3 = watcher.Address{
 		IP:   "1.1.1.3",
 		Port: 3,
-		ExternalWorkload: &ewv1alpha1.ExternalWorkload{
+		ExternalWorkload: &ewv1beta1.ExternalWorkload{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "ew-3",
 				Namespace: "ns",
@@ -154,8 +154,8 @@ var (
 					k8s.ProxyDeploymentLabel: "deployment-name",
 				},
 			},
-			Spec: ewv1alpha1.ExternalWorkloadSpec{
-				MeshTls: ewv1alpha1.MeshTls{
+			Spec: ewv1beta1.ExternalWorkloadSpec{
+				MeshTLS: ewv1beta1.MeshTLS{
 					Identity:   "spiffe://some-domain/ew-3",
 					ServerName: "server.local",
 				},
@@ -166,7 +166,7 @@ var (
 	ewOpaque = watcher.Address{
 		IP:   "1.1.1.4",
 		Port: 4,
-		ExternalWorkload: &ewv1alpha1.ExternalWorkload{
+		ExternalWorkload: &ewv1beta1.ExternalWorkload{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "pod4",
 				Namespace: "ns",
@@ -174,13 +174,13 @@ var (
 					k8s.ProxyOpaquePortsAnnotation: "4",
 				},
 			},
-			Spec: ewv1alpha1.ExternalWorkloadSpec{
-				MeshTls: ewv1alpha1.MeshTls{
+			Spec: ewv1beta1.ExternalWorkloadSpec{
+				MeshTLS: ewv1beta1.MeshTLS{
 					Identity:   "spiffe://some-domain/ew-opaque",
 					ServerName: "server.local",
 				},
 
-				Ports: []ewv1alpha1.PortSpec{
+				Ports: []ewv1beta1.PortSpec{
 					{
 						Port: 4143,
 						Name: "linkerd-proxy",
diff --git a/controller/api/destination/external-workload/endpoints_controller_test.go b/controller/api/destination/external-workload/endpoints_controller_test.go
index 46cf27e3e..ce629f00a 100644
--- a/controller/api/destination/external-workload/endpoints_controller_test.go
+++ b/controller/api/destination/external-workload/endpoints_controller_test.go
@@ -8,7 +8,7 @@ import (
 	"testing"
 	"time"
 
-	ewv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	ewv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	v1 "k8s.io/api/core/v1"
 	discoveryv1 "k8s.io/api/discovery/v1"
@@ -50,10 +50,10 @@ func newController(t *testing.T) (*k8s.API, func() []k8stesting.Action, *endpoin
 
 }
 
-func newExternalWorkload(n int, namespace string, ready bool, terminating bool) *ewv1alpha1.ExternalWorkload {
-	status := ewv1alpha1.ConditionTrue
+func newExternalWorkload(n int, namespace string, ready bool, terminating bool) *ewv1beta1.ExternalWorkload {
+	status := ewv1beta1.ConditionTrue
 	if !ready {
-		status = ewv1alpha1.ConditionFalse
+		status = ewv1beta1.ConditionFalse
 	}
 
 	var deletionTimestamp *metav1.Time
@@ -63,7 +63,7 @@ func newExternalWorkload(n int, namespace string, ready bool, terminating bool)
 		}
 	}
 
-	ew := &ewv1alpha1.ExternalWorkload{
+	ew := &ewv1beta1.ExternalWorkload{
 		TypeMeta: metav1.TypeMeta{APIVersion: "v1"},
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace:         namespace,
@@ -72,21 +72,21 @@ func newExternalWorkload(n int, namespace string, ready bool, terminating bool)
 			DeletionTimestamp: deletionTimestamp,
 			ResourceVersion:   fmt.Sprint(n),
 		},
-		Spec: ewv1alpha1.ExternalWorkloadSpec{
-			Ports: []ewv1alpha1.PortSpec{
+		Spec: ewv1beta1.ExternalWorkloadSpec{
+			Ports: []ewv1beta1.PortSpec{
 				{
 					Name: "name",
 					Port: 444,
 				},
 			},
-			WorkloadIPs: []ewv1alpha1.WorkloadIP{
+			WorkloadIPs: []ewv1beta1.WorkloadIP{
 				{Ip: "1.2.3.4"},
 			},
 		},
-		Status: ewv1alpha1.ExternalWorkloadStatus{
-			Conditions: []ewv1alpha1.WorkloadCondition{
+		Status: ewv1beta1.ExternalWorkloadStatus{
+			Conditions: []ewv1beta1.WorkloadCondition{
 				{
-					Type:   ewv1alpha1.WorkloadReady,
+					Type:   ewv1beta1.WorkloadReady,
 					Status: status,
 				},
 			},
@@ -309,10 +309,10 @@ func TestSyncServiceEndpointSlicePendingDeletion(t *testing.T) {
 	}
 }
 
-func makeExternalWorkload(resVersion, name string, labels map[string]string, ports map[int32]string, ips []string) *ewv1alpha1.ExternalWorkload {
-	portSpecs := []ewv1alpha1.PortSpec{}
+func makeExternalWorkload(resVersion, name string, labels map[string]string, ports map[int32]string, ips []string) *ewv1beta1.ExternalWorkload {
+	portSpecs := []ewv1beta1.PortSpec{}
 	for port, name := range ports {
-		spec := ewv1alpha1.PortSpec{
+		spec := ewv1beta1.PortSpec{
 			Port: port,
 		}
 		if name != "" {
@@ -321,31 +321,31 @@ func makeExternalWorkload(resVersion, name string, labels map[string]string, por
 		portSpecs = append(portSpecs, spec)
 	}
 
-	wIps := []ewv1alpha1.WorkloadIP{}
+	wIps := []ewv1beta1.WorkloadIP{}
 	for _, ip := range ips {
-		wIps = append(wIps, ewv1alpha1.WorkloadIP{Ip: ip})
+		wIps = append(wIps, ewv1beta1.WorkloadIP{Ip: ip})
 	}
 
-	ew := &ewv1alpha1.ExternalWorkload{
+	ew := &ewv1beta1.ExternalWorkload{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:            name,
 			Namespace:       "ns",
 			Labels:          labels,
 			ResourceVersion: resVersion,
 		},
-		Spec: ewv1alpha1.ExternalWorkloadSpec{
-			MeshTls: ewv1alpha1.MeshTls{
+		Spec: ewv1beta1.ExternalWorkloadSpec{
+			MeshTLS: ewv1beta1.MeshTLS{
 				Identity:   "some-identity",
 				ServerName: "some-sni",
 			},
 			Ports:       portSpecs,
 			WorkloadIPs: wIps,
 		},
-		Status: ewv1alpha1.ExternalWorkloadStatus{
-			Conditions: []ewv1alpha1.WorkloadCondition{
+		Status: ewv1beta1.ExternalWorkloadStatus{
+			Conditions: []ewv1beta1.WorkloadCondition{
 				{
-					Type:   ewv1alpha1.WorkloadReady,
-					Status: ewv1alpha1.ConditionTrue,
+					Type:   ewv1beta1.WorkloadReady,
+					Status: ewv1beta1.ConditionTrue,
 				},
 			},
 		},
@@ -361,7 +361,7 @@ func TestSyncService(t *testing.T) {
 	testcases := []struct {
 		name                  string
 		service               *v1.Service
-		externalWorkloads     []*ewv1alpha1.ExternalWorkload
+		externalWorkloads     []*ewv1beta1.ExternalWorkload
 		expectedEndpointPorts []discoveryv1.EndpointPort
 		expectedEndpoints     []discoveryv1.Endpoint
 	}{
@@ -383,7 +383,7 @@ func TestSyncService(t *testing.T) {
 					IPFamilies: []v1.IPFamily{v1.IPv4Protocol},
 				},
 			},
-			externalWorkloads: []*ewv1alpha1.ExternalWorkload{
+			externalWorkloads: []*ewv1beta1.ExternalWorkload{
 				{
 					ObjectMeta: metav1.ObjectMeta{
 						Namespace:         namespace,
@@ -391,23 +391,23 @@ func TestSyncService(t *testing.T) {
 						Labels:            map[string]string{"foo": "bar"},
 						DeletionTimestamp: nil,
 					},
-					Spec: ewv1alpha1.ExternalWorkloadSpec{
-						WorkloadIPs: []ewv1alpha1.WorkloadIP{
+					Spec: ewv1beta1.ExternalWorkloadSpec{
+						WorkloadIPs: []ewv1beta1.WorkloadIP{
 							{
 								Ip: "10.0.0.1",
 							},
 						},
-						Ports: []ewv1alpha1.PortSpec{
+						Ports: []ewv1beta1.PortSpec{
 							{Name: "tcp-example", Port: 80, Protocol: v1.ProtocolTCP},
 							{Name: "udp-example", Port: 161, Protocol: v1.ProtocolUDP},
 							{Name: "sctp-example", Port: 3456, Protocol: v1.ProtocolSCTP},
 						},
 					},
-					Status: ewv1alpha1.ExternalWorkloadStatus{
-						Conditions: []ewv1alpha1.WorkloadCondition{
+					Status: ewv1beta1.ExternalWorkloadStatus{
+						Conditions: []ewv1beta1.WorkloadCondition{
 							{
-								Type:   ewv1alpha1.WorkloadReady,
-								Status: ewv1alpha1.ConditionTrue,
+								Type:   ewv1beta1.WorkloadReady,
+								Status: ewv1beta1.ConditionTrue,
 							},
 						},
 					},
@@ -420,8 +420,8 @@ func TestSyncService(t *testing.T) {
 						DeletionTimestamp: nil,
 					},
 
-					Spec: ewv1alpha1.ExternalWorkloadSpec{
-						WorkloadIPs: []ewv1alpha1.WorkloadIP{
+					Spec: ewv1beta1.ExternalWorkloadSpec{
+						WorkloadIPs: []ewv1beta1.WorkloadIP{
 							{
 								Ip: "10.0.0.2",
 							},
@@ -429,17 +429,17 @@ func TestSyncService(t *testing.T) {
 								Ip: "fd08::5678:0000:0000:9abc:def0",
 							},
 						},
-						Ports: []ewv1alpha1.PortSpec{
+						Ports: []ewv1beta1.PortSpec{
 							{Name: "tcp-example", Port: 80, Protocol: v1.ProtocolTCP},
 							{Name: "udp-example", Port: 161, Protocol: v1.ProtocolUDP},
 							{Name: "sctp-example", Port: 3456, Protocol: v1.ProtocolSCTP},
 						},
 					},
-					Status: ewv1alpha1.ExternalWorkloadStatus{
-						Conditions: []ewv1alpha1.WorkloadCondition{
+					Status: ewv1beta1.ExternalWorkloadStatus{
+						Conditions: []ewv1beta1.WorkloadCondition{
 							{
-								Type:   ewv1alpha1.WorkloadReady,
-								Status: ewv1alpha1.ConditionTrue,
+								Type:   ewv1beta1.WorkloadReady,
+								Status: ewv1beta1.ConditionTrue,
 							},
 						},
 					},
@@ -501,7 +501,7 @@ func TestSyncService(t *testing.T) {
 					IPFamilies: []v1.IPFamily{v1.IPv6Protocol},
 				},
 			},
-			externalWorkloads: []*ewv1alpha1.ExternalWorkload{
+			externalWorkloads: []*ewv1beta1.ExternalWorkload{
 				{
 					ObjectMeta: metav1.ObjectMeta{
 						Namespace:         namespace,
@@ -509,23 +509,23 @@ func TestSyncService(t *testing.T) {
 						Labels:            map[string]string{"foo": "bar"},
 						DeletionTimestamp: nil,
 					},
-					Spec: ewv1alpha1.ExternalWorkloadSpec{
-						WorkloadIPs: []ewv1alpha1.WorkloadIP{
+					Spec: ewv1beta1.ExternalWorkloadSpec{
+						WorkloadIPs: []ewv1beta1.WorkloadIP{
 							{
 								Ip: "10.0.0.1",
 							},
 						},
-						Ports: []ewv1alpha1.PortSpec{
+						Ports: []ewv1beta1.PortSpec{
 							{Name: "tcp-example", Port: 80, Protocol: v1.ProtocolTCP},
 							{Name: "udp-example", Port: 161, Protocol: v1.ProtocolUDP},
 							{Name: "sctp-example", Port: 3456, Protocol: v1.ProtocolSCTP},
 						},
 					},
-					Status: ewv1alpha1.ExternalWorkloadStatus{
-						Conditions: []ewv1alpha1.WorkloadCondition{
+					Status: ewv1beta1.ExternalWorkloadStatus{
+						Conditions: []ewv1beta1.WorkloadCondition{
 							{
-								Type:   ewv1alpha1.WorkloadReady,
-								Status: ewv1alpha1.ConditionTrue,
+								Type:   ewv1beta1.WorkloadReady,
+								Status: ewv1beta1.ConditionTrue,
 							},
 						},
 					},
@@ -537,8 +537,8 @@ func TestSyncService(t *testing.T) {
 						Labels:            map[string]string{"foo": "bar"},
 						DeletionTimestamp: nil,
 					},
-					Spec: ewv1alpha1.ExternalWorkloadSpec{
-						WorkloadIPs: []ewv1alpha1.WorkloadIP{
+					Spec: ewv1beta1.ExternalWorkloadSpec{
+						WorkloadIPs: []ewv1beta1.WorkloadIP{
 							{
 								Ip: "10.0.0.2",
 							},
@@ -547,17 +547,17 @@ func TestSyncService(t *testing.T) {
 								Ip: "fd08::5678:0000:0000:9abc:def0",
 							},
 						},
-						Ports: []ewv1alpha1.PortSpec{
+						Ports: []ewv1beta1.PortSpec{
 							{Name: "tcp-example", Port: 80, Protocol: v1.ProtocolTCP},
 							{Name: "udp-example", Port: 161, Protocol: v1.ProtocolUDP},
 							{Name: "sctp-example", Port: 3456, Protocol: v1.ProtocolSCTP},
 						},
 					},
-					Status: ewv1alpha1.ExternalWorkloadStatus{
-						Conditions: []ewv1alpha1.WorkloadCondition{
+					Status: ewv1beta1.ExternalWorkloadStatus{
+						Conditions: []ewv1beta1.WorkloadCondition{
 							{
-								Type:   ewv1alpha1.WorkloadReady,
-								Status: ewv1alpha1.ConditionTrue,
+								Type:   ewv1beta1.WorkloadReady,
+								Status: ewv1beta1.ConditionTrue,
 							},
 						},
 					},
@@ -610,7 +610,7 @@ func TestSyncService(t *testing.T) {
 					IPFamilies: []v1.IPFamily{v1.IPv4Protocol},
 				},
 			},
-			externalWorkloads: []*ewv1alpha1.ExternalWorkload{
+			externalWorkloads: []*ewv1beta1.ExternalWorkload{
 				{
 					ObjectMeta: metav1.ObjectMeta{
 						Namespace:         namespace,
@@ -618,23 +618,23 @@ func TestSyncService(t *testing.T) {
 						Labels:            map[string]string{"foo": "bar"},
 						DeletionTimestamp: nil,
 					},
-					Spec: ewv1alpha1.ExternalWorkloadSpec{
-						WorkloadIPs: []ewv1alpha1.WorkloadIP{
+					Spec: ewv1beta1.ExternalWorkloadSpec{
+						WorkloadIPs: []ewv1beta1.WorkloadIP{
 							{
 								Ip: "10.0.0.1",
 							},
 						},
-						Ports: []ewv1alpha1.PortSpec{
+						Ports: []ewv1beta1.PortSpec{
 							{Name: "tcp-example", Port: 80, Protocol: v1.ProtocolTCP},
 							{Name: "udp-example", Port: 161, Protocol: v1.ProtocolUDP},
 							{Name: "sctp-example", Port: 3456, Protocol: v1.ProtocolSCTP},
 						},
 					},
-					Status: ewv1alpha1.ExternalWorkloadStatus{
-						Conditions: []ewv1alpha1.WorkloadCondition{
+					Status: ewv1beta1.ExternalWorkloadStatus{
+						Conditions: []ewv1beta1.WorkloadCondition{
 							{
-								Type:   ewv1alpha1.WorkloadReady,
-								Status: ewv1alpha1.ConditionTrue,
+								Type:   ewv1beta1.WorkloadReady,
+								Status: ewv1beta1.ConditionTrue,
 							},
 						},
 					},
@@ -646,8 +646,8 @@ func TestSyncService(t *testing.T) {
 						Labels:            map[string]string{"foo": "bar"},
 						DeletionTimestamp: nil,
 					},
-					Spec: ewv1alpha1.ExternalWorkloadSpec{
-						WorkloadIPs: []ewv1alpha1.WorkloadIP{
+					Spec: ewv1beta1.ExternalWorkloadSpec{
+						WorkloadIPs: []ewv1beta1.WorkloadIP{
 							{
 								Ip: "10.0.0.2",
 							},
@@ -656,17 +656,17 @@ func TestSyncService(t *testing.T) {
 								Ip: "fd08::5678:0000:0000:9abc:def0",
 							},
 						},
-						Ports: []ewv1alpha1.PortSpec{
+						Ports: []ewv1beta1.PortSpec{
 							{Name: "tcp-example", Port: 80, Protocol: v1.ProtocolTCP},
 							{Name: "udp-example", Port: 161, Protocol: v1.ProtocolUDP},
 							{Name: "sctp-example", Port: 3456, Protocol: v1.ProtocolSCTP},
 						},
 					},
-					Status: ewv1alpha1.ExternalWorkloadStatus{
-						Conditions: []ewv1alpha1.WorkloadCondition{
+					Status: ewv1beta1.ExternalWorkloadStatus{
+						Conditions: []ewv1beta1.WorkloadCondition{
 							{
-								Type:   ewv1alpha1.WorkloadReady,
-								Status: ewv1alpha1.ConditionFalse,
+								Type:   ewv1beta1.WorkloadReady,
+								Status: ewv1beta1.ConditionFalse,
 							},
 						},
 					},
@@ -728,7 +728,7 @@ func TestSyncService(t *testing.T) {
 					IPFamilies: []v1.IPFamily{v1.IPv4Protocol},
 				},
 			},
-			externalWorkloads: []*ewv1alpha1.ExternalWorkload{
+			externalWorkloads: []*ewv1beta1.ExternalWorkload{
 				{
 					ObjectMeta: metav1.ObjectMeta{
 						Namespace:         namespace,
@@ -736,23 +736,23 @@ func TestSyncService(t *testing.T) {
 						Labels:            map[string]string{"foo": "bar"},
 						DeletionTimestamp: nil,
 					},
-					Spec: ewv1alpha1.ExternalWorkloadSpec{
-						WorkloadIPs: []ewv1alpha1.WorkloadIP{
+					Spec: ewv1beta1.ExternalWorkloadSpec{
+						WorkloadIPs: []ewv1beta1.WorkloadIP{
 							{
 								Ip: "10.0.0.1",
 							},
 						},
-						Ports: []ewv1alpha1.PortSpec{
+						Ports: []ewv1beta1.PortSpec{
 							{Name: "tcp-example", Port: 80, Protocol: v1.ProtocolTCP},
 							{Name: "udp-example", Port: 161, Protocol: v1.ProtocolUDP},
 							{Name: "sctp-example", Port: 3456, Protocol: v1.ProtocolSCTP},
 						},
 					},
-					Status: ewv1alpha1.ExternalWorkloadStatus{
-						Conditions: []ewv1alpha1.WorkloadCondition{
+					Status: ewv1beta1.ExternalWorkloadStatus{
+						Conditions: []ewv1beta1.WorkloadCondition{
 							{
-								Type:   ewv1alpha1.WorkloadReady,
-								Status: ewv1alpha1.ConditionTrue,
+								Type:   ewv1beta1.WorkloadReady,
+								Status: ewv1beta1.ConditionTrue,
 							},
 						},
 					},
@@ -764,23 +764,23 @@ func TestSyncService(t *testing.T) {
 						Labels:            map[string]string{"foo": "bar"},
 						DeletionTimestamp: nil,
 					},
-					Spec: ewv1alpha1.ExternalWorkloadSpec{
-						WorkloadIPs: []ewv1alpha1.WorkloadIP{
+					Spec: ewv1beta1.ExternalWorkloadSpec{
+						WorkloadIPs: []ewv1beta1.WorkloadIP{
 							{
 								Ip: "10.0.0.1",
 							},
 						},
-						Ports: []ewv1alpha1.PortSpec{
+						Ports: []ewv1beta1.PortSpec{
 							{Name: "tcp-example", Port: 80, Protocol: v1.ProtocolTCP},
 							{Name: "udp-example", Port: 161, Protocol: v1.ProtocolUDP},
 							{Name: "sctp-example", Port: 3456, Protocol: v1.ProtocolSCTP},
 						},
 					},
-					Status: ewv1alpha1.ExternalWorkloadStatus{
-						Conditions: []ewv1alpha1.WorkloadCondition{
+					Status: ewv1beta1.ExternalWorkloadStatus{
+						Conditions: []ewv1beta1.WorkloadCondition{
 							{
-								Type:   ewv1alpha1.WorkloadReady,
-								Status: ewv1alpha1.ConditionTrue,
+								Type:   ewv1beta1.WorkloadReady,
+								Status: ewv1beta1.ConditionTrue,
 							},
 						},
 					},
@@ -885,8 +885,8 @@ func TestSyncService(t *testing.T) {
 func TestEwEndpointsChanged(t *testing.T) {
 	for _, tt := range []struct {
 		name        string
-		old         *ewv1alpha1.ExternalWorkload
-		updated     *ewv1alpha1.ExternalWorkload
+		old         *ewv1beta1.ExternalWorkload
+		updated     *ewv1beta1.ExternalWorkload
 		specChanged bool
 	}{
 		{
@@ -1067,8 +1067,8 @@ func TestEwEndpointsChanged(t *testing.T) {
 func TestWorkloadServicesToUpdate(t *testing.T) {
 	for _, tt := range []struct {
 		name           string
-		old            *ewv1alpha1.ExternalWorkload
-		updated        *ewv1alpha1.ExternalWorkload
+		old            *ewv1beta1.ExternalWorkload
+		updated        *ewv1beta1.ExternalWorkload
 		k8sConfigs     []string
 		expectServices map[string]struct{}
 	}{
@@ -1318,15 +1318,15 @@ func protoPtr(proto v1.Protocol) *v1.Protocol {
 	return &proto
 }
 
-func newStatusCondition(ready bool) ewv1alpha1.WorkloadCondition {
-	var status ewv1alpha1.WorkloadConditionStatus
+func newStatusCondition(ready bool) ewv1beta1.WorkloadCondition {
+	var status ewv1beta1.WorkloadConditionStatus
 	if ready {
-		status = ewv1alpha1.ConditionTrue
+		status = ewv1beta1.ConditionTrue
 	} else {
-		status = ewv1alpha1.ConditionFalse
+		status = ewv1beta1.ConditionFalse
 	}
-	return ewv1alpha1.WorkloadCondition{
-		Type:               ewv1alpha1.WorkloadReady,
+	return ewv1beta1.WorkloadCondition{
+		Type:               ewv1beta1.WorkloadReady,
 		Status:             status,
 		LastProbeTime:      metav1.Time{},
 		LastTransitionTime: metav1.NewTime(time.Now()),
diff --git a/controller/api/destination/external-workload/endpoints_reconciler_test.go b/controller/api/destination/external-workload/endpoints_reconciler_test.go
index 4d545d82c..3e22ff140 100644
--- a/controller/api/destination/external-workload/endpoints_reconciler_test.go
+++ b/controller/api/destination/external-workload/endpoints_reconciler_test.go
@@ -7,7 +7,7 @@ import (
 	"strings"
 	"testing"
 
-	ewv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1alpha1"
+	ewv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1"
 	"github.com/linkerd/linkerd2/controller/k8s"
 
 	corev1 "k8s.io/api/core/v1"
@@ -63,7 +63,7 @@ func TestReconcilerCreatesNewEndpointSlice(t *testing.T) {
 	ew.ObjectMeta.UID = types.UID(fmt.Sprintf("%s-%s", ew.Namespace, ew.Name))
 
 	r := newEndpointsReconciler(k8sAPI, testControllerName, defaultTestEndpointsQuota)
-	err = r.reconcile(svc, []*ewv1alpha1.ExternalWorkload{ew}, nil)
+	err = r.reconcile(svc, []*ewv1beta1.ExternalWorkload{ew}, nil)
 	if err != nil {
 		t.Fatalf("unexpected error when reconciling endpoints: %v", err)
 	}
@@ -103,7 +103,7 @@ func TestReconcilerCreatesNewEndpointSliceHeadless(t *testing.T) {
 	ew.ObjectMeta.UID = types.UID(fmt.Sprintf("%s-%s", ew.Namespace, ew.Name))
 
 	r := newEndpointsReconciler(k8sAPI, testControllerName, defaultTestEndpointsQuota)
-	err = r.reconcile(svc, []*ewv1alpha1.ExternalWorkload{ew}, nil)
+	err = r.reconcile(svc, []*ewv1beta1.ExternalWorkload{ew}, nil)
 	if err != nil {
 		t.Fatalf("unexpected error when reconciling endpoints: %v", err)
 	}
@@ -170,7 +170,7 @@ func TestReconcilerUpdatesEndpointSlice(t *testing.T) {
 	}
 
 	r := newEndpointsReconciler(k8sAPI, testControllerName, defaultTestEndpointsQuota)
-	err = r.reconcile(svc, []*ewv1alpha1.ExternalWorkload{ewCreated, ewUpdated}, []*discoveryv1.EndpointSlice{es})
+	err = r.reconcile(svc, []*ewv1beta1.ExternalWorkload{ewCreated, ewUpdated}, []*discoveryv1.EndpointSlice{es})
 	if err != nil {
 		t.Fatalf("unexpected error when reconciling endpoints: %v", err)
 	}
@@ -237,7 +237,7 @@ func TestReconcilerUpdatesEndpointSliceInPlace(t *testing.T) {
 	}
 
 	r := newEndpointsReconciler(k8sAPI, testControllerName, defaultTestEndpointsQuota)
-	err = r.reconcile(svc, []*ewv1alpha1.ExternalWorkload{ewCreated, ewCreated}, []*discoveryv1.EndpointSlice{es})
+	err = r.reconcile(svc, []*ewv1beta1.ExternalWorkload{ewCreated, ewCreated}, []*discoveryv1.EndpointSlice{es})
 	if err != nil {
 		t.Fatalf("unexpected error when reconciling endpoints: %v", err)
 	}
@@ -268,7 +268,7 @@ func TestReconcilerUpdatesEndpointSliceInPlace(t *testing.T) {
 // A named port on a service can target a different port on a workload
 func TestReconcileEndpointSlicesNamedPorts(t *testing.T) {
 	svc := makeIPv4Service(map[string]string{"app": "test"}, []corev1.ServicePort{httpNamedPort}, "192.0.2.1")
-	ews := []*ewv1alpha1.ExternalWorkload{}
+	ews := []*ewv1beta1.ExternalWorkload{}
 	// Generate a large number of external workloads
 	// randomise ports so that a named port maps to different target values
 	for i := 0; i < 300; i++ {
@@ -277,7 +277,7 @@ func TestReconcileEndpointSlicesNamedPorts(t *testing.T) {
 		genIp := fmt.Sprintf("192.%d.%d.%d", i%5, i%3, i%2)
 		genPort := int32(8080 + offset)
 		ew := makeExternalWorkload("1", fmt.Sprintf("wlkd-%d", i), map[string]string{"app": "test"}, map[int32]string{genPort: "http"}, []string{genIp})
-		ew.Status.Conditions = []ewv1alpha1.WorkloadCondition{newStatusCondition(ready)}
+		ew.Status.Conditions = []ewv1beta1.WorkloadCondition{newStatusCondition(ready)}
 		ews = append(ews, ew)
 	}
 
@@ -322,13 +322,13 @@ func TestReconcileEndpointSlicesNamedPorts(t *testing.T) {
 func TestReconcileManyWorkloads(t *testing.T) {
 	svc := makeIPv4Service(map[string]string{"app": "test"}, []corev1.ServicePort{httpUnnamedPort}, "10.0.2.1")
 	// start with 250 workloads
-	ews := []*ewv1alpha1.ExternalWorkload{}
+	ews := []*ewv1beta1.ExternalWorkload{}
 	for i := 0; i < 250; i++ {
 		ready := !(i%3 == 0)
 		genIp := fmt.Sprintf("192.%d.%d.%d", i%5, i%3, i%2)
 		ew := makeExternalWorkload("1", fmt.Sprintf("wlkd-%d", i), map[string]string{"app": "test"}, map[int32]string{8080: ""}, []string{genIp})
 
-		ew.Status.Conditions = []ewv1alpha1.WorkloadCondition{newStatusCondition(ready)}
+		ew.Status.Conditions = []ewv1beta1.WorkloadCondition{newStatusCondition(ready)}
 		ews = append(ews, ew)
 	}
 
@@ -350,13 +350,13 @@ func TestReconcileManyWorkloads(t *testing.T) {
 func TestReconcileEndpointSlicesSomePreexisting(t *testing.T) {
 	svc := makeIPv4Service(map[string]string{"app": "test"}, []corev1.ServicePort{httpUnnamedPort}, "10.0.2.1")
 	// start with 250 workloads
-	ews := []*ewv1alpha1.ExternalWorkload{}
+	ews := []*ewv1beta1.ExternalWorkload{}
 	for i := 0; i < 250; i++ {
 		ready := !(i%3 == 0)
 		genIp := fmt.Sprintf("192.%d.%d.%d", i%5, i%3, i%2)
 		ew := makeExternalWorkload("1", fmt.Sprintf("wlkd-%d", i), map[string]string{"app": "test"}, map[int32]string{8080: ""}, []string{genIp})
 
-		ew.Status.Conditions = []ewv1alpha1.WorkloadCondition{newStatusCondition(ready)}
+		ew.Status.Conditions = []ewv1beta1.WorkloadCondition{newStatusCondition(ready)}
 		ews = append(ews, ew)
 	}
 
@@ -408,13 +408,13 @@ func TestReconcileEndpointSlicesSomePreexisting(t *testing.T) {
 func TestReconcileEndpointSlicesUpdatingSvc(t *testing.T) {
 	svc := makeIPv4Service(map[string]string{"app": "test"}, []corev1.ServicePort{httpUnnamedPort}, "10.0.2.1")
 	// start with 250 workloads
-	ews := []*ewv1alpha1.ExternalWorkload{}
+	ews := []*ewv1beta1.ExternalWorkload{}
 	for i := 0; i < 250; i++ {
 		ready := !(i%3 == 0)
 		genIp := fmt.Sprintf("192.%d.%d.%d", i%5, i%3, i%2)
 		ew := makeExternalWorkload("1", fmt.Sprintf("wlkd-%d", i), map[string]string{"app": "test"}, map[int32]string{8080: ""}, []string{genIp})
 
-		ew.Status.Conditions = []ewv1alpha1.WorkloadCondition{newStatusCondition(ready)}
+		ew.Status.Conditions = []ewv1beta1.WorkloadCondition{newStatusCondition(ready)}
 		ews = append(ews, ew)
 	}
 
@@ -447,13 +447,13 @@ func TestReconcileEndpointSlicesUpdatingSvc(t *testing.T) {
 func TestReconcileEndpointSlicesLabelsUpdatingSvc(t *testing.T) {
 	svc := makeIPv4Service(map[string]string{"app": "test"}, []corev1.ServicePort{httpUnnamedPort}, "10.0.2.1")
 	// start with 250 workloads
-	ews := []*ewv1alpha1.ExternalWorkload{}
+	ews := []*ewv1beta1.ExternalWorkload{}
 	for i := 0; i < 250; i++ {
 		ready := !(i%3 == 0)
 		genIp := fmt.Sprintf("192.%d.%d.%d", i%5, i%3, i%2)
 		ew := makeExternalWorkload("1", fmt.Sprintf("wlkd-%d", i), map[string]string{"app": "test"}, map[int32]string{8080: ""}, []string{genIp})
 
-		ew.Status.Conditions = []ewv1alpha1.WorkloadCondition{newStatusCondition(ready)}
+		ew.Status.Conditions = []ewv1beta1.WorkloadCondition{newStatusCondition(ready)}
 		ews = append(ews, ew)
 	}
 
@@ -487,13 +487,13 @@ func TestReconcileEndpointSlicesLabelsUpdatingSvc(t *testing.T) {
 func TestReconcileEndpointSlicesReservedLabelsSvc(t *testing.T) {
 	svc := makeIPv4Service(map[string]string{"app": "test"}, []corev1.ServicePort{httpUnnamedPort}, "10.0.2.1")
 	// start with 250 workloads
-	ews := []*ewv1alpha1.ExternalWorkload{}
+	ews := []*ewv1beta1.ExternalWorkload{}
 	for i := 0; i < 250; i++ {
 		ready := !(i%3 == 0)
 		genIp := fmt.Sprintf("192.%d.%d.%d", i%5, i%3, i%2)
 		ew := makeExternalWorkload("1", fmt.Sprintf("wlkd-%d", i), map[string]string{"app": "test"}, map[int32]string{8080: ""}, []string{genIp})
 
-		ew.Status.Conditions = []ewv1alpha1.WorkloadCondition{newStatusCondition(ready)}
+		ew.Status.Conditions = []ewv1beta1.WorkloadCondition{newStatusCondition(ready)}
 		ews = append(ews, ew)
 	}
 
@@ -535,13 +535,13 @@ func TestReconcileEndpointSlicesReservedLabelsSvc(t *testing.T) {
 func TestEndpointSlicesAreRecycled(t *testing.T) {
 	svc := makeIPv4Service(map[string]string{"app": "test"}, []corev1.ServicePort{httpUnnamedPort}, "10.0.2.1")
 	// start with 250 workloads
-	ews := []*ewv1alpha1.ExternalWorkload{}
+	ews := []*ewv1beta1.ExternalWorkload{}
 	for i := 0; i < 300; i++ {
 		ready := !(i%3 == 0)
 		genIp := fmt.Sprintf("192.%d.%d.%d", i%5, i%3, i%2)
 		ew := makeExternalWorkload("1", fmt.Sprintf("wlkd-%d", i), map[string]string{"app": "test"}, map[int32]string{8080: ""}, []string{genIp})
 
-		ew.Status.Conditions = []ewv1alpha1.WorkloadCondition{newStatusCondition(ready)}
+		ew.Status.Conditions = []ewv1beta1.WorkloadCondition{newStatusCondition(ready)}
 		ews = append(ews, ew)
 	}
 
@@ -723,7 +723,7 @@ func makeIPv4Service(selector map[string]string, ports []corev1.ServicePort, clu
 	}
 }
 
-func makeEndpoint(addrs []string, isReady bool, ew *ewv1alpha1.ExternalWorkload) discoveryv1.Endpoint {
+func makeEndpoint(addrs []string, isReady bool, ew *ewv1beta1.ExternalWorkload) discoveryv1.Endpoint {
 	rdy := &isReady
 	term := !isReady
 	ep := discoveryv1.Endpoint{
diff --git a/controller/api/destination/test_util.go b/controller/api/destination/test_util.go
index 25d5161cb..0f7286e3c 100644
--- a/controller/api/destination/test_util.go
+++ b/controller/api/destination/test_util.go
@@ -508,7 +508,7 @@ spec:
 	}
 
 	externalWorkloads := []string{`
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: my-cool-workload
@@ -516,7 +516,7 @@ metadata:
   annotations:
     config.linkerd.io/opaque-ports: "4242"
 spec:
-  meshTls:
+  meshTLS:
     identity: spiffe://some-domain/cool
     serverName: server.local
   workloadIPs:
@@ -530,7 +530,7 @@ status:
   conditions:
   ready: true`,
 		`
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: policy-test-workload
@@ -538,7 +538,7 @@ metadata:
   labels:
     app: external-workload-policy-test
 spec:
-  meshTls:
+  meshTLS:
     identity: spiffe://some-domain/cool
     serverName: server.local
   workloadIPs:
diff --git a/controller/api/destination/watcher/endpoints_watcher_test.go b/controller/api/destination/watcher/endpoints_watcher_test.go
index b7981a00e..3c1c20ae1 100644
--- a/controller/api/destination/watcher/endpoints_watcher_test.go
+++ b/controller/api/destination/watcher/endpoints_watcher_test.go
@@ -1434,7 +1434,7 @@ ports:
 - name: ""
   port: 8989`,
 				`
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: name-1-1
@@ -1443,7 +1443,7 @@ status:
   conditions:
   ready: true`,
 				`
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: name-1-2
@@ -1452,7 +1452,7 @@ status:
   conditions:
   ready: true`,
 				`
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: name-1-3
@@ -1546,7 +1546,7 @@ metadata:
 ports:
 - name: ""
   port: 8989`, `
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: name-1-3
@@ -1614,7 +1614,7 @@ metadata:
 ports:
 - name: ""
   port: 8989`, `
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: name-1-1
@@ -1686,7 +1686,7 @@ metadata:
 ports:
 - name: ""
   port: 9000`, `
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: name-5-1
@@ -2095,7 +2095,7 @@ metadata:
 ports:
 - name: ""
   port: 8989`, `
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: name1-1
@@ -2126,7 +2126,7 @@ metadata:
   namespace: ns
 ports:
 - name: ""
-  port: 8989`, `apiVersion: workload.linkerd.io/v1alpha1
+  port: 8989`, `apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: name1-2
@@ -2962,13 +2962,13 @@ metadata:
 status:
   phase: Running
   podIP: 172.17.0.12`, `
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: wlkd1
   namespace: ns
 spec:
-  meshTls:
+  meshTLS:
     identity: foo
     serverName: foo
   ports:
@@ -3131,13 +3131,13 @@ metadata:
 status:
   phase: Running
   podIP: 172.17.0.13`, `
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: wlkd1
   namespace: ns
 spec:
-  meshTls:
+  meshTLS:
     identity: foo
     serverName: foo
   ports:
@@ -3149,13 +3149,13 @@ status:
   - type: Ready
     status: "True"
 `, `
-apiVersion: workload.linkerd.io/v1alpha1
+apiVersion: workload.linkerd.io/v1beta1
 kind: ExternalWorkload
 metadata:
   name: wlkd2
   namespace: ns
 spec:
-  meshTls:
+  meshTLS:
     identity: foo
     serverName: foo
   ports: