Repository: linkerd/linkerd2
Pull request #11899 (2024-01-08 15:01:45)

policy: add externalWorkloadSelector to Server resource
This PR adds the ability for a `Server` resource to select over `ExternalWorkload`
resources in addition to `Pods`. For the time being, only one of these selector types
can be specified. This has been realized via incrementing the version of the resource
to `v1beta2`

Signed-off-by: Zahari Dichev <zaharidichev@gmail.com>


Patch:
diff --git a/charts/linkerd-crds/templates/policy/server.yaml b/charts/linkerd-crds/templates/policy/server.yaml
index a5d9f26da..ac3215c02 100644
--- a/charts/linkerd-crds/templates/policy/server.yaml
+++ b/charts/linkerd-crds/templates/policy/server.yaml
@@ -72,7 +72,9 @@ spec:
                   default: unknown
     - name: v1beta1
       served: true
-      storage: true
+      storage: false
+      deprecated: true
+      deprecationWarning: "policy.linkerd.io/v1alpha1 Server is deprecated; use policy.linkerd.io/v1beta2 Server"
       schema:
         openAPIV3Schema:
           type: object
@@ -130,3 +132,91 @@ spec:
         type: string
         description: The protocol of the server
         jsonPath: .spec.proxyProtocol
+    - name: v1beta2
+      served: true
+      storage: true
+      schema:
+        openAPIV3Schema:
+          type: object
+          required: [spec]
+          properties:
+            spec:
+              type: object
+              required:
+                - port
+              oneOf:
+                - required: [podSelector]
+                - required: [externalWorkloadSelector]
+              properties:
+                podSelector:
+                  type: object
+                  description: >-
+                    Selects pods in the same namespace.
+
+                    The result of matchLabels and matchExpressions are ANDed.
+                    Selects all if empty.
+                  properties:
+                    matchLabels:
+                      type: object
+                      x-kubernetes-preserve-unknown-fields: true
+                    matchExpressions:
+                      type: array
+                      items:
+                        type: object
+                        required: [key, operator]
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                            enum: [In, NotIn, Exists, DoesNotExist]
+                          values:
+                            type: array
+                            items:
+                              type: string
+                externalWorkloadSelector:
+                  type: object
+                  description: >-
+                    Selects ExternalWorkloads in the same namespace.
+
+                    The result of matchLabels and matchExpressions are ANDed.
+                    Selects all if empty.
+                  properties:
+                    matchLabels:
+                      type: object
+                      x-kubernetes-preserve-unknown-fields: true
+                    matchExpressions:
+                      type: array
+                      items:
+                        type: object
+                        required: [key, operator]
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                            enum: [In, NotIn, Exists, DoesNotExist]
+                          values:
+                            type: array
+                            items:
+                              type: string
+                port:
+                  description: >-
+                    A port name or number. Must exist in a pod spec.
+                  x-kubernetes-int-or-string: true
+                proxyProtocol:
+                  description: >-
+                    Configures protocol discovery for inbound connections.
+
+                    Supersedes the `config.linkerd.io/opaque-ports` annotation.
+                  type: string
+                  default: unknown
+      additionalPrinterColumns:
+      - name: Port
+        type: string
+        description: The port the server is listening on
+        jsonPath: .spec.port
+      - name: Protocol
+        type: string
+        description: The protocol of the server
+        jsonPath: .spec.proxyProtocol
diff --git a/cli/cmd/endpoints.go b/cli/cmd/endpoints.go
index 1e835d918..a25238b00 100644
--- a/cli/cmd/endpoints.go
+++ b/cli/cmd/endpoints.go
@@ -37,6 +37,8 @@ type (
 		name    string
 		address string
 		ip      string
+		weight  uint32
+		labels  map[string]string
 	}
 )
 
@@ -200,6 +202,8 @@ func requestEndpointsFromAPI(client destinationPb.DestinationClient, authorities
 					name:    labels["pod"],
 					address: tcpAddr.String(),
 					ip:      getIP(tcpAddr),
+					weight:  addr.GetWeight(),
+					labels:  addr.GetMetricLabels(),
 				})
 			}
 		}
@@ -230,6 +234,9 @@ type rowEndpoint struct {
 	Port      uint32 `json:"port"`
 	Pod       string `json:"pod"`
 	Service   string `json:"service"`
+	Weight    uint32 `json:"weight"`
+
+	Labels map[string]string `json:"labels"`
 }
 
 func writeEndpointsToBuffer(endpoints endpointsInfo, w *tabwriter.Writer, options *endpointsOptions) {
@@ -255,6 +262,8 @@ func writeEndpointsToBuffer(endpoints endpointsInfo, w *tabwriter.Writer, option
 					Port:      port,
 					Pod:       name,
 					Service:   serviceID,
+					Weight:    pod.weight,
+					Labels:    pod.labels,
 				}
 
 				endpointsTables[namespace] = append(endpointsTables[namespace], row)
diff --git a/policy-controller/k8s/api/src/policy/server.rs b/policy-controller/k8s/api/src/policy/server.rs
index 5c40f1459..82a68ec75 100644
--- a/policy-controller/k8s/api/src/policy/server.rs
+++ b/policy-controller/k8s/api/src/policy/server.rs
@@ -8,17 +8,26 @@ use std::{fmt, num::NonZeroU16};
 #[derive(Clone, Debug, PartialEq, Eq, CustomResource, Deserialize, Serialize, JsonSchema)]
 #[kube(
     group = "policy.linkerd.io",
-    version = "v1beta1",
+    version = "v1beta2",
     kind = "Server",
     namespaced
 )]
 #[serde(rename_all = "camelCase")]
 pub struct ServerSpec {
-    pub pod_selector: labels::Selector,
+    #[serde(flatten)]
+    pub selector: Selector,
     pub port: Port,
     pub proxy_protocol: Option<ProxyProtocol>,
 }
 
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
+pub enum Selector {
+    #[serde(rename = "podSelector")]
+    Pod(labels::Selector),
+    #[serde(rename = "externalWorkloadSelector")]
+    ExternalWorkload(labels::Selector),
+}
+
 /// References a pod spec's port by name or number.
 #[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
 #[serde(untagged)]
diff --git a/policy-controller/k8s/index/src/inbound/index.rs b/policy-controller/k8s/index/src/inbound/index.rs
index 576d92d34..8b94c1c49 100644
--- a/policy-controller/k8s/index/src/inbound/index.rs
+++ b/policy-controller/k8s/index/src/inbound/index.rs
@@ -25,7 +25,9 @@ use linkerd_policy_controller_core::{
     },
     IdentityMatch, Ipv4Net, Ipv6Net, NetworkMatch,
 };
-use linkerd_policy_controller_k8s_api::{self as k8s, policy::server::Port, ResourceExt};
+use linkerd_policy_controller_k8s_api::{
+    self as k8s, policy::server::Port, policy::server::Selector, ResourceExt,
+};
 use parking_lot::RwLock;
 use std::{
     collections::{hash_map::Entry, BTreeSet},
@@ -924,34 +926,36 @@ impl Pod {
         );
 
         for (srvname, server) in policy.servers.iter() {
-            if server.pod_selector.matches(&self.meta.labels) {
-                for port in self.select_ports(&server.port_ref).into_iter() {
-                    // If the port is already matched to a server, then log a warning and skip
-                    // updating it so it doesn't flap between servers.
-                    if let Some(prior) = matched_ports.get(&port) {
-                        tracing::warn!(
-                            port = %port,
-                            server = %prior,
-                            conflict = %srvname,
-                            "Port already matched by another server; skipping"
-                        );
-                        continue;
-                    }
+            if let Selector::Pod(pod_selector) = &server.selector {
+                if pod_selector.matches(&self.meta.labels) {
+                    for port in self.select_ports(&server.port_ref).into_iter() {
+                        // If the port is already matched to a server, then log a warning and skip
+                        // updating it so it doesn't flap between servers.
+                        if let Some(prior) = matched_ports.get(&port) {
+                            tracing::warn!(
+                                port = %port,
+                                server = %prior,
+                                conflict = %srvname,
+                                "Port already matched by another server; skipping"
+                            );
+                            continue;
+                        }
 
-                    let s = policy.inbound_server(
-                        srvname.clone(),
-                        server,
-                        authentications,
-                        self.probes
-                            .get(&port)
-                            .into_iter()
-                            .flatten()
-                            .map(|p| p.as_str()),
-                    );
-                    self.update_server(port, srvname, s);
+                        let s = policy.inbound_server(
+                            srvname.clone(),
+                            server,
+                            authentications,
+                            self.probes
+                                .get(&port)
+                                .into_iter()
+                                .flatten()
+                                .map(|p| p.as_str()),
+                        );
+                        self.update_server(port, srvname, s);
 
-                    matched_ports.insert(port, srvname.clone());
-                    unmatched_ports.remove(&port);
+                        matched_ports.insert(port, srvname.clone());
+                        unmatched_ports.remove(&port);
+                    }
                 }
             }
         }
diff --git a/policy-controller/k8s/index/src/inbound/server.rs b/policy-controller/k8s/index/src/inbound/server.rs
index 67ca821b6..d017655e0 100644
--- a/policy-controller/k8s/index/src/inbound/server.rs
+++ b/policy-controller/k8s/index/src/inbound/server.rs
@@ -1,12 +1,14 @@
 use crate::ClusterInfo;
 use linkerd_policy_controller_core::inbound::ProxyProtocol;
-use linkerd_policy_controller_k8s_api::{self as k8s, policy::server::Port};
+use linkerd_policy_controller_k8s_api::{
+    self as k8s, policy::server::Port, policy::server::Selector,
+};
 
 /// The parts of a `Server` resource that can change.
 #[derive(Debug, PartialEq)]
 pub(crate) struct Server {
     pub labels: k8s::Labels,
-    pub pod_selector: k8s::labels::Selector,
+    pub selector: Selector,
     pub port_ref: Port,
     pub protocol: ProxyProtocol,
 }
@@ -15,7 +17,7 @@ impl Server {
     pub(crate) fn from_resource(srv: k8s::policy::Server, cluster: &ClusterInfo) -> Self {
         Self {
             labels: srv.metadata.labels.into(),
-            pod_selector: srv.spec.pod_selector,
+            selector: srv.spec.selector,
             port_ref: srv.spec.port,
             protocol: proxy_protocol(srv.spec.proxy_protocol, cluster),
         }
diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs
index 63eb41cf2..06ceafbb3 100644
--- a/policy-controller/src/admission.rs
+++ b/policy-controller/src/admission.rs
@@ -1,12 +1,9 @@
 use super::validation;
-use crate::k8s::{
-    labels,
-    policy::{
-        httproute, AuthorizationPolicy, AuthorizationPolicySpec, HttpRoute, HttpRouteSpec,
-        LocalTargetRef, MeshTLSAuthentication, MeshTLSAuthenticationSpec, NamespacedTargetRef,
-        NetworkAuthentication, NetworkAuthenticationSpec, Server, ServerAuthorization,
-        ServerAuthorizationSpec, ServerSpec,
-    },
+use crate::k8s::policy::{
+    httproute, server::Selector, AuthorizationPolicy, AuthorizationPolicySpec, HttpRoute,
+    HttpRouteSpec, LocalTargetRef, MeshTLSAuthentication, MeshTLSAuthenticationSpec,
+    NamespacedTargetRef, NetworkAuthentication, NetworkAuthenticationSpec, Server,
+    ServerAuthorization, ServerAuthorizationSpec, ServerSpec,
 };
 use anyhow::{anyhow, bail, ensure, Result};
 use futures::future;
@@ -328,7 +325,7 @@ impl Validate<ServerSpec> for Admission {
             let server_name = server.name_unchecked();
             if server_name != name
                 && server.spec.port == spec.port
-                && Self::overlaps(&server.spec.pod_selector, &spec.pod_selector)
+                && Self::overlaps(&server.spec.selector, &spec.selector)
             {
                 let server_ns = server.namespace();
                 let server_ns = server_ns.as_deref().unwrap_or("default");
@@ -351,9 +348,19 @@ impl Admission {
     // example, if `left` selects pods with 'foo=bar' and `right` selects pods with
     // 'foo', we should indicate the selectors overlap. It's a bit tricky to work
     // through all of the cases though, so we'll just punt for now.
-    fn overlaps(left: &labels::Selector, right: &labels::Selector) -> bool {
-        if left.selects_all() || right.selects_all() {
-            return true;
+    fn overlaps(left: &Selector, right: &Selector) -> bool {
+        match (left, right) {
+            (Selector::Pod(ps_left), Selector::Pod(ps_right)) => {
+                if ps_left.selects_all() || ps_right.selects_all() {
+                    return true;
+                }
+            }
+            (Selector::ExternalWorkload(et_left), Selector::ExternalWorkload(et_right)) => {
+                if et_left.selects_all() || et_right.selects_all() {
+                    return true;
+                }
+            }
+            (_, _) => return false,
         }
 
         left == right


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