Repository: linkerd/linkerd2
Pull request #12784 (2024-06-27 22:57:42)

Refactor inbound policy index in preparation for grpc support
Refactor inbound policy index in preparation for grpc support.  No functional changes.  

<!--  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/Cargo.lock b/Cargo.lock
index b7f2a8921..11880db5a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -244,9 +244,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
 
 [[package]]
 name = "cc"
-version = "1.0.101"
+version = "1.0.103"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d"
+checksum = "2755ff20a1d93490d26ba33a6f092a38a508398a5320df5d4b3014fcccce9410"
 
 [[package]]
 name = "cfg-if"
@@ -269,9 +269,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.7"
+version = "4.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
+checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -279,9 +279,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.7"
+version = "4.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
+checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708"
 dependencies = [
  "anstyle",
  "clap_lex",
@@ -289,9 +289,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.5"
+version = "4.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
+checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -1351,9 +1351,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.21"
+version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
 
 [[package]]
 name = "maplit"
@@ -1439,9 +1439,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.36.0"
+version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434"
+checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
 dependencies = [
  "memchr",
 ]
@@ -2042,9 +2042,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.118"
+version = "1.0.119"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
+checksum = "e8eddb61f0697cc3989c5d64b452f5488e2b8a60fd7d5076a3045076ffef8cb0"
 dependencies = [
  "itoa",
  "ryu",
diff --git a/charts/linkerd-crds/templates/workload/external-workload.yaml b/charts/linkerd-crds/templates/workload/external-workload.yaml
index a60aff48b..715e779aa 100644
--- a/charts/linkerd-crds/templates/workload/external-workload.yaml
+++ b/charts/linkerd-crds/templates/workload/external-workload.yaml
@@ -17,7 +17,6 @@ spec:
     listKind: ExternalWorkloadList
     plural: externalworkloads
     singular: externalworkload
-    shortNames: []
   scope: Namespaced
   versions:
   - name: v1alpha1
diff --git a/go.mod b/go.mod
index c92166d8a..5e5797d88 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/fatih/color v1.17.0
 	github.com/fsnotify/fsnotify v1.7.0
 	github.com/go-openapi/spec v0.21.0
-	github.com/go-test/deep v1.1.0
+	github.com/go-test/deep v1.1.1
 	github.com/golang/protobuf v1.5.4
 	github.com/gorilla/websocket v1.5.3
 	github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b
diff --git a/go.sum b/go.sum
index 720ab9171..6927421da 100644
--- a/go.sum
+++ b/go.sum
@@ -162,8 +162,8 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
-github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
-github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
+github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
+github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
diff --git a/policy-controller/core/src/inbound.rs b/policy-controller/core/src/inbound.rs
index 7f128a508..c50663552 100644
--- a/policy-controller/core/src/inbound.rs
+++ b/policy-controller/core/src/inbound.rs
@@ -26,9 +26,9 @@ pub enum AuthorizationRef {
 }
 
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
-pub enum HttpRouteRef {
+pub enum RouteRef {
     Default(&'static str),
-    Linkerd(GroupKindName),
+    Resource(GroupKindName),
 }
 
 /// Describes how a proxy should handle inbound connections.
@@ -89,13 +89,15 @@ pub struct InboundServer {
 
     pub protocol: ProxyProtocol,
     pub authorizations: HashMap<AuthorizationRef, ClientAuthorization>,
-    pub http_routes: HashMap<HttpRouteRef, HttpRoute>,
+    pub http_routes: HashMap<RouteRef, InboundRoute<HttpRouteMatch>>,
 }
 
+pub type HttpRoute = InboundRoute<HttpRouteMatch>;
+
 #[derive(Clone, Debug, PartialEq, Eq)]
-pub struct HttpRoute {
+pub struct InboundRoute<M> {
     pub hostnames: Vec<HostMatch>,
-    pub rules: Vec<HttpRouteRule>,
+    pub rules: Vec<InboundRouteRule<M>>,
     pub authorizations: HashMap<AuthorizationRef, ClientAuthorization>,
 
     /// This is required for ordering returned `HttpRoute`s by their creation
@@ -104,8 +106,8 @@ pub struct HttpRoute {
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
-pub struct HttpRouteRule {
-    pub matches: Vec<HttpRouteMatch>,
+pub struct InboundRouteRule<M> {
+    pub matches: Vec<M>,
     pub filters: Vec<Filter>,
 }
 
@@ -117,15 +119,15 @@ pub enum Filter {
     FailureInjector(FailureInjectorFilter),
 }
 
-// === impl InboundHttpRoute ===
+// === impl InboundRoute ===
 
-/// The default `InboundHttpRoute` used for any `InboundServer` that
+/// The default `InboundRoute` used for any `InboundServer` that
 /// does not have routes.
-impl Default for HttpRoute {
+impl Default for InboundRoute<HttpRouteMatch> {
     fn default() -> Self {
         Self {
             hostnames: vec![],
-            rules: vec![HttpRouteRule {
+            rules: vec![InboundRouteRule {
                 matches: vec![HttpRouteMatch {
                     path: Some(PathMatch::Prefix("/".to_string())),
                     headers: vec![],
@@ -145,20 +147,20 @@ impl Default for HttpRoute {
 
 // === impl InboundHttpRouteRef ===
 
-impl Ord for HttpRouteRef {
+impl Ord for RouteRef {
     fn cmp(&self, other: &Self) -> std::cmp::Ordering {
         match (self, other) {
             (Self::Default(a), Self::Default(b)) => a.cmp(b),
-            (Self::Linkerd(a), Self::Linkerd(b)) => a.cmp(b),
+            (Self::Resource(a), Self::Resource(b)) => a.cmp(b),
             // Route resources are always preferred over default resources, so they should sort
             // first in a list.
-            (Self::Linkerd(_), Self::Default(_)) => std::cmp::Ordering::Less,
-            (Self::Default(_), Self::Linkerd(_)) => std::cmp::Ordering::Greater,
+            (Self::Resource(_), Self::Default(_)) => std::cmp::Ordering::Less,
+            (Self::Default(_), Self::Resource(_)) => std::cmp::Ordering::Greater,
         }
     }
 }
 
-impl PartialOrd for HttpRouteRef {
+impl PartialOrd for RouteRef {
     fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
         Some(self.cmp(other))
     }
diff --git a/policy-controller/grpc/src/inbound.rs b/policy-controller/grpc/src/inbound.rs
index 036ddf6cc..5c2ddbf94 100644
--- a/policy-controller/grpc/src/inbound.rs
+++ b/policy-controller/grpc/src/inbound.rs
@@ -1,4 +1,4 @@
-use crate::{routes, workload::Workload};
+use crate::workload::Workload;
 use futures::prelude::*;
 use linkerd2_proxy_api::{
     self as api,
@@ -10,9 +10,8 @@ use linkerd2_proxy_api::{
 };
 use linkerd_policy_controller_core::{
     inbound::{
-        AuthorizationRef, ClientAuthentication, ClientAuthorization, DiscoverInboundServer, Filter,
-        HttpRoute, HttpRouteRef, HttpRouteRule, InboundServer, InboundServerStream, ProxyProtocol,
-        ServerRef,
+        AuthorizationRef, ClientAuthentication, ClientAuthorization, DiscoverInboundServer,
+        InboundServer, InboundServerStream, ProxyProtocol, ServerRef,
     },
     IdentityMatch, IpNet, NetworkMatch,
 };
@@ -20,6 +19,8 @@ use maplit::*;
 use std::{num::NonZeroU16, str::FromStr, sync::Arc};
 use tracing::trace;
 
+mod http;
+
 #[derive(Clone, Debug)]
 pub struct InboundPolicyServer<T> {
     discover: T,
@@ -144,21 +145,23 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server {
             ProxyProtocol::Detect { timeout } => Some(proto::proxy_protocol::Kind::Detect(
                 proto::proxy_protocol::Detect {
                     timeout: timeout.try_into().map_err(|error| tracing::warn!(%error, "failed to convert protocol detect timeout to protobuf")).ok(),
-                    http_routes: to_http_route_list(&srv.http_routes, cluster_networks),
+                    http_routes: http::to_route_list(&srv.http_routes, cluster_networks),
                 },
             )),
             ProxyProtocol::Http1 => Some(proto::proxy_protocol::Kind::Http1(
                 proto::proxy_protocol::Http1 {
-                    routes: to_http_route_list(&srv.http_routes, cluster_networks),
+                    routes: http::to_route_list(&srv.http_routes, cluster_networks),
                 },
             )),
             ProxyProtocol::Http2 => Some(proto::proxy_protocol::Kind::Http2(
                 proto::proxy_protocol::Http2 {
-                    routes: to_http_route_list(&srv.http_routes, cluster_networks),
+                    routes: http::to_route_list(&srv.http_routes, cluster_networks),
                 },
             )),
-            ProxyProtocol::Grpc => Some(proto::proxy_protocol::Kind::Grpc(
-                proto::proxy_protocol::Grpc::default(),
+            ProxyProtocol::Grpc => Some(proto::proxy_protocol::Kind::Http2(
+                proto::proxy_protocol::Http2 {
+                    routes: http::to_route_list(&srv.http_routes, cluster_networks),
+                },
             )),
             ProxyProtocol::Opaque => Some(proto::proxy_protocol::Kind::Opaque(
                 proto::proxy_protocol::Opaque {},
@@ -323,107 +326,3 @@ fn to_authz(
         authentication: Some(authn),
     }
 }
-
-fn to_http_route_list<'r>(
-    routes: impl IntoIterator<Item = (&'r HttpRouteRef, &'r HttpRoute)>,
-    cluster_networks: &[IpNet],
-) -> Vec<proto::HttpRoute> {
-    // Per the Gateway API spec:
-    //
-    // > If ties still exist across multiple Routes, matching precedence MUST be
-    // > determined in order of the following criteria, continuing on ties:
-    // >
-    // >    The oldest Route based on creation timestamp.
-    // >    The Route appearing first in alphabetical order by
-    // >   "{namespace}/{name}".
-    //
-    // Note that we don't need to include the route's namespace in this
-    // comparison, because all these routes will exist in the same
-    // namespace.
-    let mut route_list = routes.into_iter().collect::<Vec<_>>();
-    route_list.sort_by(|(a_ref, a), (b_ref, b)| {
-        let by_ts = match (&a.creation_timestamp, &b.creation_timestamp) {
-            (Some(a_ts), Some(b_ts)) => a_ts.cmp(b_ts),
-            (None, None) => std::cmp::Ordering::Equal,
-            // Routes with timestamps are preferred over routes without.
-            (Some(_), None) => return std::cmp::Ordering::Less,
-            (None, Some(_)) => return std::cmp::Ordering::Greater,
-        };
-        by_ts.then_with(|| a_ref.cmp(b_ref))
-    });
-
-    route_list
-        .into_iter()
-        .map(|(route_ref, route)| to_http_route(route_ref, route.clone(), cluster_networks))
-        .collect()
-}
-
-fn to_http_route(
-    reference: &HttpRouteRef,
-    HttpRoute {
-        hostnames,
-        rules,
-        authorizations,
-        creation_timestamp: _,
-    }: HttpRoute,
-    cluster_networks: &[IpNet],
-) -> proto::HttpRoute {
-    let metadata = Metadata {
-        kind: Some(match reference {
-            HttpRouteRef::Default(name) => metadata::Kind::Default(name.to_string()),
-            HttpRouteRef::Linkerd(gkn) => metadata::Kind::Resource(api::meta::Resource {
-                group: gkn.group.to_string(),
-                kind: gkn.kind.to_string(),
-                name: gkn.name.to_string(),
-                ..Default::default()
-            }),
-        }),
-    };
-
-    let hosts = hostnames
-        .into_iter()
-        .map(routes::convert_host_match)
-        .collect();
-
-    let rules = rules
-        .into_iter()
-        .map(
-            |HttpRouteRule { matches, filters }| proto::http_route::Rule {
-                matches: matches
-                    .into_iter()
-                    .map(routes::http::convert_match)
-                    .collect(),
-                filters: filters.into_iter().filter_map(convert_filter).collect(),
-            },
-        )
-        .collect();
-
-    let authorizations = authorizations
-        .iter()
-        .map(|(n, c)| to_authz(n, c, cluster_networks))
-        .collect();
-
-    proto::HttpRoute {
-        metadata: Some(metadata),
-        hosts,
-        rules,
-        authorizations,
-    }
-}
-
-fn convert_filter(filter: Filter) -> Option<proto::http_route::Filter> {
-    use proto::http_route::filter::Kind;
-
-    let kind = match filter {
-        Filter::FailureInjector(f) => Some(Kind::FailureInjector(
-            routes::http::convert_failure_injector_filter(f),
-        )),
-        Filter::RequestHeaderModifier(f) => Some(Kind::RequestHeaderModifier(
-            routes::convert_request_header_modifier_filter(f),
-        )),
-        Filter::ResponseHeaderModifier(_) => None,
-        Filter::RequestRedirect(f) => Some(Kind::Redirect(routes::convert_redirect_filter(f))),
-    };
-
-    kind.map(|kind| proto::http_route::Filter { kind: Some(kind) })
-}
diff --git a/policy-controller/grpc/src/inbound/http.rs b/policy-controller/grpc/src/inbound/http.rs
new file mode 100644
index 000000000..f8b50e999
--- /dev/null
+++ b/policy-controller/grpc/src/inbound/http.rs
@@ -0,0 +1,114 @@
+use super::to_authz;
+use crate::routes;
+use linkerd2_proxy_api::{inbound, meta};
+use linkerd_policy_controller_core::{
+    inbound::{Filter, HttpRoute, InboundRouteRule, RouteRef},
+    IpNet,
+};
+
+pub(crate) fn to_route_list<'r>(
+    routes: impl IntoIterator<Item = (&'r RouteRef, &'r HttpRoute)>,
+    cluster_networks: &[IpNet],
+) -> Vec<inbound::HttpRoute> {
+    // Per the Gateway API spec:
+    //
+    // > If ties still exist across multiple Routes, matching precedence MUST be
+    // > determined in order of the following criteria, continuing on ties:
+    // >
+    // >    The oldest Route based on creation timestamp.
+    // >    The Route appearing first in alphabetical order by
+    // >   "{namespace}/{name}".
+    //
+    // Note that we don't need to include the route's namespace in this
+    // comparison, because all these routes will exist in the same
+    // namespace.
+    let mut route_list = routes.into_iter().collect::<Vec<_>>();
+    route_list.sort_by(|(a_ref, a), (b_ref, b)| {
+        let by_ts = match (&a.creation_timestamp, &b.creation_timestamp) {
+            (Some(a_ts), Some(b_ts)) => a_ts.cmp(b_ts),
+            (None, None) => std::cmp::Ordering::Equal,
+            // Routes with timestamps are preferred over routes without.
+            (Some(_), None) => return std::cmp::Ordering::Less,
+            (None, Some(_)) => return std::cmp::Ordering::Greater,
+        };
+        by_ts.then_with(|| a_ref.cmp(b_ref))
+    });
+
+    route_list
+        .into_iter()
+        .map(|(route_ref, route)| to_http_route(route_ref, route.clone(), cluster_networks))
+        .collect()
+}
+
+fn to_http_route(
+    reference: &RouteRef,
+    HttpRoute {
+        hostnames,
+        rules,
+        authorizations,
+        creation_timestamp: _,
+    }: HttpRoute,
+    cluster_networks: &[IpNet],
+) -> inbound::HttpRoute {
+    let metadata = meta::Metadata {
+        kind: Some(match reference {
+            RouteRef::Default(name) => meta::metadata::Kind::Default(name.to_string()),
+            RouteRef::Resource(gkn) => meta::metadata::Kind::Resource(meta::Resource {
+                group: gkn.group.to_string(),
+                kind: gkn.kind.to_string(),
+                name: gkn.name.to_string(),
+                ..Default::default()
+            }),
+        }),
+    };
+
+    let hosts = hostnames
+        .into_iter()
+        .map(routes::convert_host_match)
+        .collect();
+
+    let rules = rules
+        .into_iter()
+        .map(
+            |InboundRouteRule { matches, filters }| inbound::http_route::Rule {
+                matches: matches
+                    .into_iter()
+                    .map(routes::http::convert_match)
+                    .collect(),
+                filters: filters
+                    .into_iter()
+                    .filter_map(convert_http_filter)
+                    .collect(),
+            },
+        )
+        .collect();
+
+    let authorizations = authorizations
+        .iter()
+        .map(|(n, c)| to_authz(n, c, cluster_networks))
+        .collect();
+
+    inbound::HttpRoute {
+        metadata: Some(metadata),
+        hosts,
+        rules,
+        authorizations,
+    }
+}
+
+fn convert_http_filter(filter: Filter) -> Option<inbound::http_route::Filter> {
+    use inbound::http_route::filter::Kind;
+
+    let kind = match filter {
+        Filter::FailureInjector(f) => Some(Kind::FailureInjector(
+            routes::http::convert_failure_injector_filter(f),
+        )),
+        Filter::RequestHeaderModifier(f) => Some(Kind::RequestHeaderModifier(
+            routes::convert_request_header_modifier_filter(f),
+        )),
+        Filter::ResponseHeaderModifier(_) => None,
+        Filter::RequestRedirect(f) => Some(Kind::Redirect(routes::convert_redirect_filter(f))),
+    };
+
+    kind.map(|kind| inbound::http_route::Filter { kind: Some(kind) })
+}
diff --git a/policy-controller/grpc/src/outbound.rs b/policy-controller/grpc/src/outbound.rs
index dd771fd37..7d01cb001 100644
--- a/policy-controller/grpc/src/outbound.rs
+++ b/policy-controller/grpc/src/outbound.rs
@@ -18,7 +18,7 @@ use linkerd_policy_controller_core::{
     },
     routes::GroupKindNamespaceName,
 };
-use std::{iter, num::NonZeroU16, str::FromStr, sync::Arc, time};
+use std::{num::NonZeroU16, str::FromStr, sync::Arc, time};
 
 mod grpc;
 mod http;
@@ -231,25 +231,14 @@ fn to_service(outbound: OutboundPolicy) -> outbound::OutboundPolicy {
         });
 
         let mut http_routes = outbound.http_routes.into_iter().collect::<Vec<_>>();
-        http_routes.sort_by(timestamp_then_name);
-        let mut http_routes = http_routes.into_iter().peekable();
-
         let mut grpc_routes = outbound.grpc_routes.into_iter().collect::<Vec<_>>();
-        grpc_routes.sort_by(timestamp_then_name);
-        let mut grpc_routes = grpc_routes.into_iter().peekable();
-
-        // If both HTTP and gRPC routes are present, we choose the route kind by which has the oldest timestamp.
-        match (http_routes.peek(), grpc_routes.peek()) {
-            (Some(http), Some(grpc)) => {
-                if timestamp_then_name(http, grpc).is_gt() {
-                    grpc::protocol(backend, grpc_routes, accrual)
-                } else {
-                    http::protocol(backend, http_routes, accrual)
-                }
-            }
-            (Some((_, _http)), None) => http::protocol(backend, http_routes, accrual),
-            (None, Some((_, _grpc))) => grpc::protocol(backend, grpc_routes, accrual),
-            (None, None) => http::protocol(backend, iter::empty(), accrual),
+
+        if !grpc_routes.is_empty() {
+            grpc_routes.sort_by(timestamp_then_name);
+            grpc::protocol(backend, grpc_routes.into_iter(), accrual)
+        } else {
+            http_routes.sort_by(timestamp_then_name);
+            http::protocol(backend, http_routes.into_iter(), accrual)
         }
     };
 
diff --git a/policy-controller/k8s/index/src/inbound.rs b/policy-controller/k8s/index/src/inbound.rs
index 8aa0b218a..cadbda11f 100644
--- a/policy-controller/k8s/index/src/inbound.rs
+++ b/policy-controller/k8s/index/src/inbound.rs
@@ -1,8 +1,8 @@
 pub mod authorization_policy;
-mod http_route;
 pub mod index;
 mod meshtls_authentication;
 mod network_authentication;
+mod routes;
 mod server;
 pub mod server_authorization;
 mod workload;
diff --git a/policy-controller/k8s/index/src/inbound/http_route.rs b/policy-controller/k8s/index/src/inbound/http_route.rs
deleted file mode 100644
index 812e077b7..000000000
--- a/policy-controller/k8s/index/src/inbound/http_route.rs
+++ /dev/null
@@ -1,376 +0,0 @@
-use ahash::AHashMap as HashMap;
-use anyhow::{bail, Error, Result};
-use k8s_gateway_api as api;
-use linkerd_policy_controller_core::{
-    inbound::{Filter, HttpRoute, HttpRouteRule},
-    routes::{HttpRouteMatch, Method},
-    POLICY_CONTROLLER_NAME,
-};
-use linkerd_policy_controller_k8s_api::{
-    self as k8s, gateway, gateway as k8s_gateway_api,
-    policy::{httproute as policy, Server},
-};
-use std::fmt;
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct RouteBinding {
-    pub parents: Vec<ParentRef>,
-    pub route: HttpRoute,
-    pub statuses: Vec<Status>,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum ParentRef {
-    Server(String),
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct Status {
-    pub parent: ParentRef,
-    pub conditions: Vec<Condition>,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct Condition {
-    pub type_: ConditionType,
-    pub status: bool,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum ConditionType {
-    Accepted,
-}
-
-#[derive(Clone, Debug, thiserror::Error)]
-pub enum InvalidParentRef {
-    #[error("HTTPRoute resource may not reference a parent Server in an other namespace")]
-    ServerInAnotherNamespace,
-
-    #[error("HTTPRoute resource may not reference a parent by port")]
-    SpecifiesPort,
-
-    #[error("HTTPRoute resource may not reference a parent by section name")]
-    SpecifiesSection,
-}
-
-impl TryFrom<api::HttpRoute> for RouteBinding {
-    type Error = Error;
-
-    fn try_from(route: api::HttpRoute) -> Result<Self, Self::Error> {
-        let route_ns = route.metadata.namespace.as_deref();
-        let creation_timestamp = route.metadata.creation_timestamp.map(|k8s::Time(t)| t);
-        let parents = ParentRef::collect_from(route_ns, route.spec.inner.parent_refs)?;
-        let hostnames = route
-            .spec
-            .hostnames
-            .into_iter()
-            .flatten()
-            .map(crate::routes::http::host_match)
-            .collect();
-
-        let rules = route
-            .spec
-            .rules
-            .into_iter()
-            .flatten()
-            .map(
-                |api::HttpRouteRule {
-                     matches,
-                     filters,
-                     backend_refs: _,
-                 }| Self::try_rule(matches, filters, Self::try_gateway_filter),
-            )
-            .collect::<Result<_>>()?;
-
-        let statuses = route
-            .status
-            .map_or_else(Vec::new, |status| Status::collect_from(status.inner));
-
-        Ok(RouteBinding {
-            parents,
-            route: HttpRoute {
-                hostnames,
-                rules,
-                authorizations: HashMap::default(),
-                creation_timestamp,
-            },
-            statuses,
-        })
-    }
-}
-
-impl TryFrom<policy::HttpRoute> for RouteBinding {
-    type Error = Error;
-
-    fn try_from(route: policy::HttpRoute) -> Result<Self, Self::Error> {
-        let route_ns = route.metadata.namespace.as_deref();
-        let creation_timestamp = route.metadata.creation_timestamp.map(|k8s::Time(t)| t);
-        let parents = ParentRef::collect_from(route_ns, route.spec.inner.parent_refs)?;
-        let hostnames = route
-            .spec
-            .hostnames
-            .into_iter()
-            .flatten()
-            .map(crate::routes::http::host_match)
-            .collect();
-
-        let rules = route
-            .spec
-            .rules
-            .into_iter()
-            .flatten()
-            .map(
-                |policy::HttpRouteRule {
-                     matches, filters, ..
-                 }| { Self::try_rule(matches, filters, Self::try_policy_filter) },
-            )
-            .collect::<Result<_>>()?;
-
-        let statuses = route
-            .status
-            .map_or_else(Vec::new, |status| Status::collect_from(status.inner));
-
-        Ok(RouteBinding {
-            parents,
-            route: HttpRoute {
-                hostnames,
-                rules,
-                authorizations: HashMap::default(),
-                creation_timestamp,
-            },
-            statuses,
-        })
-    }
-}
-
-impl RouteBinding {
-    #[inline]
-    pub fn selects_server(&self, name: &str) -> bool {
-        self.parents
-            .iter()
-            .any(|p| matches!(p, ParentRef::Server(n) if n == name))
-    }
-
-    #[inline]
-    pub fn accepted_by_server(&self, name: &str) -> bool {
-        self.statuses.iter().any(|status| {
-            status.parent == ParentRef::Server(name.to_string())
-                && status
-                    .conditions
-                    .iter()
-                    .any(|condition| condition.type_ == ConditionType::Accepted && condition.status)
-        })
-    }
-
-    pub fn try_match(
-        api::HttpRouteMatch {
-            path,
-            headers,
-            query_params,
-            method,
-        }: api::HttpRouteMatch,
-    ) -> Result<HttpRouteMatch> {
-        let path = path.map(crate::routes::http::path_match).transpose()?;
-
-        let headers = headers
-            .into_iter()
-            .flatten()
-            .map(crate::routes::http::header_match)
-            .collect::<Result<_>>()?;
-
-        let query_params = query_params
-            .into_iter()
-            .flatten()
-            .map(crate::routes::http::query_param_match)
-            .collect::<Result<_>>()?;
-
-        let method = method.as_deref().map(Method::try_from).transpose()?;
-
-        Ok(HttpRouteMatch {
-            path,
-            headers,
-            query_params,
-            method,
-        })
-    }
-
-    fn try_rule<F>(
-        matches: Option<Vec<api::HttpRouteMatch>>,
-        filters: Option<Vec<F>>,
-        try_filter: impl Fn(F) -> Result<Filter>,
-    ) -> Result<HttpRouteRule> {
-        let matches = matches
-            .into_iter()
-            .flatten()
-            .map(Self::try_match)
-            .collect::<Result<_>>()?;
-
-        let filters = filters
-            .into_iter()
-            .flatten()
-            .map(try_filter)
-            .collect::<Result<_>>()?;
-
-        Ok(HttpRouteRule { matches, filters })
-    }
-
-    fn try_gateway_filter(filter: api::HttpRouteFilter) -> Result<Filter> {
-        let filter = match filter {
-            api::HttpRouteFilter::RequestHeaderModifier {
-                request_header_modifier,
-            } => {
-                let filter = crate::routes::http::header_modifier(request_header_modifier)?;
-                Filter::RequestHeaderModifier(filter)
-            }
-
-            api::HttpRouteFilter::ResponseHeaderModifier {
-                response_header_modifier,
-            } => {
-                let filter = crate::routes::http::header_modifier(response_header_modifier)?;
-                Filter::ResponseHeaderModifier(filter)
-            }
-
-            api::HttpRouteFilter::RequestRedirect { request_redirect } => {
-                let filter = crate::routes::http::req_redirect(request_redirect)?;
-                Filter::RequestRedirect(filter)
-            }
-
-            api::HttpRouteFilter::RequestMirror { .. } => {
-                bail!("RequestMirror filter is not supported")
-            }
-            api::HttpRouteFilter::URLRewrite { .. } => {
-                bail!("URLRewrite filter is not supported")
-            }
-            api::HttpRouteFilter::ExtensionRef { .. } => {
-                bail!("ExtensionRef filter is not supported")
-            }
-        };
-        Ok(filter)
-    }
-
-    fn try_policy_filter(filter: policy::HttpRouteFilter) -> Result<Filter> {
-        let filter = match filter {
-            policy::HttpRouteFilter::RequestHeaderModifier {
-                request_header_modifier,
-            } => {
-                let filter = crate::routes::http::header_modifier(request_header_modifier)?;
-                Filter::RequestHeaderModifier(filter)
-            }
-
-            policy::HttpRouteFilter::ResponseHeaderModifier {
-                response_header_modifier,
-            } => {
-                let filter = crate::routes::http::header_modifier(response_header_modifier)?;
-                Filter::ResponseHeaderModifier(filter)
-            }
-
-            policy::HttpRouteFilter::RequestRedirect { request_redirect } => {
-                let filter = crate::routes::http::req_redirect(request_redirect)?;
-                Filter::RequestRedirect(filter)
-            }
-        };
-        Ok(filter)
-    }
-}
-
-impl ParentRef {
-    fn collect_from(
-        route_ns: Option<&str>,
-        parent_refs: Option<Vec<api::ParentReference>>,
-    ) -> Result<Vec<Self>, InvalidParentRef> {
-        let parents = parent_refs
-            .into_iter()
-            .flatten()
-            .filter_map(|parent_ref| Self::from_parent_ref(route_ns, parent_ref))
-            .collect::<Result<Vec<_>, InvalidParentRef>>()?;
-
-        Ok(parents)
-    }
-
-    fn from_parent_ref(
-        route_ns: Option<&str>,
-        parent_ref: api::ParentReference,
-    ) -> Option<Result<Self, InvalidParentRef>> {
-        // Skip parent refs that don't target a `Server` resource.
-        if !policy::parent_ref_targets_kind::<Server>(&parent_ref) || parent_ref.name.is_empty() {
-            return None;
-        }
-
-        let api::ParentReference {
-            group: _,
-            kind: _,
-            namespace,
-            name,
-            section_name,
-            port,
-        } = parent_ref;
-
-        if namespace.is_some() && namespace.as_deref() != route_ns {
-            return Some(Err(InvalidParentRef::ServerInAnotherNamespace));
-        }
-        if port.is_some() {
-            return Some(Err(InvalidParentRef::SpecifiesPort));
-        }
-        if section_name.is_some() {
-            return Some(Err(InvalidParentRef::SpecifiesSection));
-        }
-
-        Some(Ok(ParentRef::Server(name)))
-    }
-}
-
-impl Status {
-    pub fn collect_from(status: gateway::RouteStatus) -> Vec<Self> {
-        status
-            .parents
-            .iter()
-            .filter(|status| status.controller_name == POLICY_CONTROLLER_NAME)
-            .filter_map(Self::from_parent_status)
-            .collect::<Vec<_>>()
-    }
-
-    fn from_parent_status(status: &gateway::RouteParentStatus) -> Option<Self> {
-        // Only match parent statuses that belong to resources of
-        // `kind: Server`.
-        match status.parent_ref.kind.as_deref() {
-            Some("Server") => (),
-            _ => return None,
-        }
-
-        let conditions = status
-            .conditions
-            .iter()
-            .filter_map(|condition| {
-                let type_ = match condition.type_.as_ref() {
-                    "Accepted" => ConditionType::Accepted,
-                    condition_type => {
-                        tracing::error!(%status.parent_ref.name, %condition_type, "Unexpected condition type found in parent status");
-                        return None;
-                    }
-                };
-                let status = match condition.status.as_ref() {
-                    "True" => true,
-                    "False" => false,
-                    condition_status => {
-                        tracing::error!(%status.parent_ref.name, %type_, %condition_status, "Unexpected condition status found in parent status");
-                        return None
-                    },
-                };
-                Some(Condition { type_, status })
-            })
-            .collect();
-
-        Some(Status {
-            parent: ParentRef::Server(status.parent_ref.name.to_string()),
-            conditions,
-        })
-    }
-}
-
-impl fmt::Display for ConditionType {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            Self::Accepted => write!(f, "Accepted"),
-        }
-    }
-}
diff --git a/policy-controller/k8s/index/src/inbound/index.rs b/policy-controller/k8s/index/src/inbound/index.rs
index e48cd651b..5e90db4c2 100644
--- a/policy-controller/k8s/index/src/inbound/index.rs
+++ b/policy-controller/k8s/index/src/inbound/index.rs
@@ -7,7 +7,7 @@
 //! kubernetes resources.
 
 use super::{
-    authorization_policy, http_route::RouteBinding, meshtls_authentication, network_authentication,
+    authorization_policy, meshtls_authentication, network_authentication, routes::RouteBinding,
     server, server_authorization, workload,
 };
 use crate::{
@@ -19,8 +19,8 @@ use ahash::{AHashMap as HashMap, AHashSet as HashSet};
 use anyhow::{anyhow, bail, Result};
 use linkerd_policy_controller_core::{
     inbound::{
-        AuthorizationRef, ClientAuthentication, ClientAuthorization, HttpRoute, HttpRouteRef,
-        HttpRouteRule, InboundServer, ProxyProtocol, ServerRef,
+        AuthorizationRef, ClientAuthentication, ClientAuthorization, HttpRoute, InboundRouteRule,
+        InboundServer, ProxyProtocol, RouteRef, ServerRef,
     },
     routes::{GroupKindName, HttpRouteMatch, Method, PathMatch},
     IdentityMatch, Ipv4Net, Ipv6Net, NetworkMatch,
@@ -38,6 +38,7 @@ use std::{
 use tokio::sync::watch;
 use tracing::info_span;
 
+mod http;
 pub mod metrics;
 
 pub type SharedIndex = Arc<RwLock<Index>>;
@@ -160,7 +161,7 @@ struct PolicyIndex {
     server_authorizations: HashMap<String, server_authorization::ServerAuthz>,
 
     authorization_policies: HashMap<String, authorization_policy::Spec>,
-    http_routes: HashMap<GroupKindName, RouteBinding>,
+    http_routes: HashMap<GroupKindName, RouteBinding<HttpRoute>>,
 }
 
 #[derive(Debug, Default)]
@@ -264,16 +265,16 @@ impl Index {
         }
     }
 
-    fn apply_route<R>(&mut self, route: R)
+    fn apply_http_route<R>(&mut self, route: R)
     where
         R: ResourceExt<DynamicType = ()>,
-        RouteBinding: TryFrom<R>,
-        <RouteBinding as TryFrom<R>>::Error: std::fmt::Display,
+        RouteBinding<HttpRoute>: TryFrom<R>,
+        <RouteBinding<HttpRoute> as TryFrom<R>>::Error: std::fmt::Display,
     {
         let ns = route.namespace().expect("HttpRoute must have a namespace");
         let name = route.name_unchecked();
         let gkn = route.gkn();
-        let _span = info_span!("apply", %ns, %name).entered();
+        let _span = info_span!("apply httproute", %ns, %name).entered();
 
         let route_binding = match route.try_into() {
             Ok(binding) => binding,
@@ -286,17 +287,17 @@ impl Index {
         self.ns_or_default_with_reindex(ns, |ns| ns.policy.update_http_route(gkn, route_binding))
     }
 
-    fn reset_route<R>(&mut self, routes: Vec<R>, deleted: HashMap<String, HashSet<String>>)
+    fn reset_http_route<R>(&mut self, routes: Vec<R>, deleted: HashMap<String, HashSet<String>>)
     where
         R: ResourceExt<DynamicType = ()>,
-        RouteBinding: TryFrom<R>,
-        <RouteBinding as TryFrom<R>>::Error: std::fmt::Display,
+        RouteBinding<HttpRoute>: TryFrom<R>,
+        <RouteBinding<HttpRoute> as TryFrom<R>>::Error: std::fmt::Display,
     {
-        let _span = info_span!("reset").entered();
+        let _span = info_span!("httproute reset").entered();
 
         // Aggregate all of the updates by namespace so that we only reindex
         // once per namespace.
-        type Ns = NsUpdate<GroupKindName, RouteBinding>;
+        type Ns = NsUpdate<GroupKindName, RouteBinding<HttpRoute>>;
         let mut updates_by_ns = HashMap::<String, Ns>::default();
         for route in routes.into_iter() {
             let namespace = route.namespace().expect("HttpRoute must be namespaced");
@@ -355,8 +356,8 @@ impl Index {
         }
     }
 
-    fn delete_route(&mut self, ns: String, gkn: GroupKindName) {
-        let _span = info_span!("delete", %ns, route = ?gkn).entered();
+    fn delete_http_route(&mut self, ns: String, gkn: GroupKindName) {
+        let _span = info_span!("delete httproute", %ns, route = ?gkn).entered();
         self.ns_with_reindex(ns, |ns| ns.policy.http_routes.remove(&gkn).is_some())
     }
 }
@@ -850,12 +851,12 @@ impl kubert::index::IndexNamespacedResource<k8s::policy::NetworkAuthentication>
 
 impl kubert::index::IndexNamespacedResource<k8s::policy::HttpRoute> for Index {
     fn apply(&mut self, route: k8s::policy::HttpRoute) {
-        self.apply_route(route)
+        self.apply_http_route(route)
     }
 
     fn delete(&mut self, ns: String, name: String) {
         let gkn = name.gkn::<k8s::policy::HttpRoute>();
-        self.delete_route(ns, gkn)
+        self.delete_http_route(ns, gkn)
     }
 
     fn reset(
@@ -863,18 +864,18 @@ impl kubert::index::IndexNamespacedResource<k8s::policy::HttpRoute> for Index {
         routes: Vec<k8s::policy::HttpRoute>,
         deleted: HashMap<String, HashSet<String>>,
     ) {
-        self.reset_route(routes, deleted)
+        self.reset_http_route(routes, deleted)
     }
 }
 
 impl kubert::index::IndexNamespacedResource<k8s_gateway_api::HttpRoute> for Index {
     fn apply(&mut self, route: k8s_gateway_api::HttpRoute) {
-        self.apply_route(route)
+        self.apply_http_route(route)
     }
 
     fn delete(&mut self, ns: String, name: String) {
         let gkn = name.gkn::<k8s_gateway_api::HttpRoute>();
-        self.delete_route(ns, gkn)
+        self.delete_http_route(ns, gkn)
     }
 
     fn reset(
@@ -882,7 +883,7 @@ impl kubert::index::IndexNamespacedResource<k8s_gateway_api::HttpRoute> for Inde
         routes: Vec<k8s_gateway_api::HttpRoute>,
         deleted: HashMap<String, HashSet<String>>,
     ) {
-        self.reset_route(routes, deleted)
+        self.reset_http_route(routes, deleted)
     }
 }
 
@@ -1609,7 +1610,7 @@ impl PolicyIndex {
                 }
                 authorization_policy::Target::Namespace => {}
                 authorization_policy::Target::HttpRoute(_) => {
-                    // Policies which target HttpRoutes will be attached to
+                    // Policies which target routes will be attached to
                     // the route authorizations and should not be included in
                     // the server authorizations.
                     continue;
@@ -1654,7 +1655,14 @@ impl PolicyIndex {
         for (name, spec) in &self.authorization_policies {
             // Skip the policy if it doesn't apply to the route.
             match &spec.target {
-                authorization_policy::Target::HttpRoute(n) if n.eq_ignore_ascii_case(gkn) => {}
+                authorization_policy::Target::HttpRoute(n) if n.eq_ignore_ascii_case(gkn) => {
+                    tracing::trace!(
+                        ns = %self.namespace,
+                        authorizationpolicy = %name,
+                        route = ?gkn,
+                        "AuthorizationPolicy targets HttpRoute",
+                    );
+                }
                 _ => {
                     tracing::trace!(
                         ns = %self.namespace,
@@ -1667,12 +1675,6 @@ impl PolicyIndex {
                 }
             }
 
-            tracing::trace!(
-                ns = %self.namespace,
-                authorizationpolicy = %name,
-                route = ?gkn,
-                "AuthorizationPolicy targets HttpRoute",
-            );
             tracing::trace!(authns = ?spec.authentications);
 
             let authz = match self.policy_client_authz(spec, authentications) {
@@ -1700,7 +1702,7 @@ impl PolicyIndex {
         server_name: &str,
         authentications: &AuthenticationNsIndex,
         probe_paths: impl Iterator<Item = &'p str>,
-    ) -> HashMap<HttpRouteRef, HttpRoute> {
+    ) -> HashMap<RouteRef, HttpRoute> {
         let routes = self
             .http_routes
             .iter()
@@ -1709,7 +1711,7 @@ impl PolicyIndex {
             .map(|(gkn, route)| {
                 let mut route = route.route.clone();
                 route.authorizations = self.route_client_authzs(gkn, authentications);
-                (HttpRouteRef::Linkerd(gkn.clone()), route)
+                (RouteRef::Resource(gkn.clone()), route)
             })
             .collect::<HashMap<_, _>>();
         if !routes.is_empty() {
@@ -1823,7 +1825,7 @@ impl PolicyIndex {
         })
     }
 
-    fn update_http_route(&mut self, gkn: GroupKindName, route: RouteBinding) -> bool {
+    fn update_http_route(&mut self, gkn: GroupKindName, route: RouteBinding<HttpRoute>) -> bool {
         match self.http_routes.entry(gkn) {
             Entry::Vacant(entry) => {
                 entry.insert(route);
@@ -1911,13 +1913,13 @@ impl ClusterInfo {
     fn default_inbound_http_routes<'p>(
         &self,
         probe_paths: impl Iterator<Item = &'p str>,
-    ) -> HashMap<HttpRouteRef, HttpRoute> {
+    ) -> HashMap<RouteRef, HttpRoute> {
         let mut routes = HashMap::with_capacity(2);
 
         // If no routes are defined for the server, use a default route that
         // matches all requests. Default authorizations are instrumented on
         // the server.
-        routes.insert(HttpRouteRef::Default("default"), HttpRoute::default());
+        routes.insert(RouteRef::Default("default"), HttpRoute::default());
 
         // If there are no probe networks, there are no probe routes to
         // authorize.
@@ -1958,14 +1960,14 @@ impl ClusterInfo {
 
         let probe_route = HttpRoute {
             hostnames: Vec::new(),
-            rules: vec![HttpRouteRule {
+            rules: vec![InboundRouteRule {
                 matches,
                 filters: Vec::new(),
             }],
             authorizations,
             creation_timestamp: None,
         };
-        routes.insert(HttpRouteRef::Default("probe"), probe_route);
+        routes.insert(RouteRef::Default("probe"), probe_route);
 
         routes
     }
diff --git a/policy-controller/k8s/index/src/inbound/index/http.rs b/policy-controller/k8s/index/src/inbound/index/http.rs
new file mode 100644
index 000000000..fa52a1a69
--- /dev/null
+++ b/policy-controller/k8s/index/src/inbound/index/http.rs
@@ -0,0 +1,209 @@
+use crate::inbound::routes::{ParentRef, RouteBinding, Status};
+use ahash::AHashMap as HashMap;
+use anyhow::{bail, Error, Result};
+use http::Method;
+use linkerd_policy_controller_core::{
+    inbound::{Filter, HttpRoute, InboundRoute, InboundRouteRule},
+    routes::HttpRouteMatch,
+};
+use linkerd_policy_controller_k8s_api::{self as k8s, gateway, policy};
+
+impl TryFrom<gateway::HttpRoute> for RouteBinding<HttpRoute> {
+    type Error = Error;
+
+    fn try_from(route: gateway::HttpRoute) -> Result<Self, Self::Error> {
+        let route_ns = route.metadata.namespace.as_deref();
+        let creation_timestamp = route.metadata.creation_timestamp.map(|k8s::Time(t)| t);
+        let parents = ParentRef::collect_from(route_ns, route.spec.inner.parent_refs)?;
+        let hostnames = route
+            .spec
+            .hostnames
+            .into_iter()
+            .flatten()
+            .map(crate::routes::http::host_match)
+            .collect();
+
+        let rules = route
+            .spec
+            .rules
+            .into_iter()
+            .flatten()
+            .map(
+                |gateway::HttpRouteRule {
+                     matches,
+                     filters,
+                     backend_refs: _,
+                 }| try_http_rule(matches, filters, try_gateway_filter),
+            )
+            .collect::<Result<_>>()?;
+
+        let statuses = route
+            .status
+            .map_or_else(Vec::new, |status| Status::collect_from(status.inner));
+
+        Ok(RouteBinding {
+            parents,
+            route: InboundRoute {
+                hostnames,
+                rules,
+                authorizations: HashMap::default(),
+                creation_timestamp,
+            },
+            statuses,
+        })
+    }
+}
+
+impl TryFrom<policy::HttpRoute> for RouteBinding<HttpRoute> {
+    type Error = Error;
+
+    fn try_from(route: policy::HttpRoute) -> Result<Self, Self::Error> {
+        let route_ns = route.metadata.namespace.as_deref();
+        let creation_timestamp = route.metadata.creation_timestamp.map(|k8s::Time(t)| t);
+        let parents = ParentRef::collect_from(route_ns, route.spec.inner.parent_refs)?;
+        let hostnames = route
+            .spec
+            .hostnames
+            .into_iter()
+            .flatten()
+            .map(crate::routes::http::host_match)
+            .collect();
+
+        let rules = route
+            .spec
+            .rules
+            .into_iter()
+            .flatten()
+            .map(
+                |policy::httproute::HttpRouteRule {
+                     matches, filters, ..
+                 }| { try_http_rule(matches, filters, try_policy_filter) },
+            )
+            .collect::<Result<_>>()?;
+
+        let statuses = route
+            .status
+            .map_or_else(Vec::new, |status| Status::collect_from(status.inner));
+
+        Ok(RouteBinding {
+            parents,
+            route: InboundRoute {
+                hostnames,
+                rules,
+                authorizations: HashMap::default(),
+                creation_timestamp,
+            },
+            statuses,
+        })
+    }
+}
+
+pub fn try_http_match(
+    gateway::HttpRouteMatch {
+        path,
+        headers,
+        query_params,
+        method,
+    }: gateway::HttpRouteMatch,
+) -> Result<HttpRouteMatch> {
+    let path = path.map(crate::routes::http::path_match).transpose()?;
+
+    let headers = headers
+        .into_iter()
+        .flatten()
+        .map(crate::routes::http::header_match)
+        .collect::<Result<_>>()?;
+
+    let query_params = query_params
+        .into_iter()
+        .flatten()
+        .map(crate::routes::http::query_param_match)
+        .collect::<Result<_>>()?;
+
+    let method = method.as_deref().map(Method::try_from).transpose()?;
+
+    Ok(HttpRouteMatch {
+        path,
+        headers,
+        query_params,
+        method,
+    })
+}
+
+fn try_http_rule<F>(
+    matches: Option<Vec<gateway::HttpRouteMatch>>,
+    filters: Option<Vec<F>>,
+    try_filter: impl Fn(F) -> Result<Filter>,
+) -> Result<InboundRouteRule<HttpRouteMatch>> {
+    let matches = matches
+        .into_iter()
+        .flatten()
+        .map(try_http_match)
+        .collect::<Result<_>>()?;
+
+    let filters = filters
+        .into_iter()
+        .flatten()
+        .map(try_filter)
+        .collect::<Result<_>>()?;
+
+    Ok(InboundRouteRule { matches, filters })
+}
+
+fn try_gateway_filter(filter: gateway::HttpRouteFilter) -> Result<Filter> {
+    let filter = match filter {
+        gateway::HttpRouteFilter::RequestHeaderModifier {
+            request_header_modifier,
+        } => {
+            let filter = crate::routes::http::header_modifier(request_header_modifier)?;
+            Filter::RequestHeaderModifier(filter)
+        }
+
+        gateway::HttpRouteFilter::ResponseHeaderModifier {
+            response_header_modifier,
+        } => {
+            let filter = crate::routes::http::header_modifier(response_header_modifier)?;
+            Filter::ResponseHeaderModifier(filter)
+        }
+
+        gateway::HttpRouteFilter::RequestRedirect { request_redirect } => {
+            let filter = crate::routes::http::req_redirect(request_redirect)?;
+            Filter::RequestRedirect(filter)
+        }
+
+        gateway::HttpRouteFilter::RequestMirror { .. } => {
+            bail!("RequestMirror filter is not supported")
+        }
+        gateway::HttpRouteFilter::URLRewrite { .. } => {
+            bail!("URLRewrite filter is not supported")
+        }
+        gateway::HttpRouteFilter::ExtensionRef { .. } => {
+            bail!("ExtensionRef filter is not supported")
+        }
+    };
+    Ok(filter)
+}
+
+fn try_policy_filter(filter: policy::httproute::HttpRouteFilter) -> Result<Filter> {
+    let filter = match filter {
+        policy::httproute::HttpRouteFilter::RequestHeaderModifier {
+            request_header_modifier,
+        } => {
+            let filter = crate::routes::http::header_modifier(request_header_modifier)?;
+            Filter::RequestHeaderModifier(filter)
+        }
+
+        policy::httproute::HttpRouteFilter::ResponseHeaderModifier {
+            response_header_modifier,
+        } => {
+            let filter = crate::routes::http::header_modifier(response_header_modifier)?;
+            Filter::ResponseHeaderModifier(filter)
+        }
+
+        policy::httproute::HttpRouteFilter::RequestRedirect { request_redirect } => {
+            let filter = crate::routes::http::req_redirect(request_redirect)?;
+            Filter::RequestRedirect(filter)
+        }
+    };
+    Ok(filter)
+}
diff --git a/policy-controller/k8s/index/src/inbound/routes.rs b/policy-controller/k8s/index/src/inbound/routes.rs
new file mode 100644
index 000000000..5338b13ef
--- /dev/null
+++ b/policy-controller/k8s/index/src/inbound/routes.rs
@@ -0,0 +1,169 @@
+use anyhow::Result;
+use linkerd_policy_controller_core::POLICY_CONTROLLER_NAME;
+use linkerd_policy_controller_k8s_api::{gateway, policy};
+use std::fmt;
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct RouteBinding<R> {
+    pub parents: Vec<ParentRef>,
+    pub route: R,
+    pub statuses: Vec<Status>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum ParentRef {
+    Server(String),
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Status {
+    pub parent: ParentRef,
+    pub conditions: Vec<Condition>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Condition {
+    pub type_: ConditionType,
+    pub status: bool,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum ConditionType {
+    Accepted,
+}
+
+#[derive(Clone, Debug, thiserror::Error)]
+pub enum InvalidParentRef {
+    #[error("route resource may not reference a parent Server in an other namespace")]
+    ServerInAnotherNamespace,
+
+    #[error("route resource may not reference a parent by port")]
+    SpecifiesPort,
+
+    #[error("route resource may not reference a parent by section name")]
+    SpecifiesSection,
+}
+
+impl<R> RouteBinding<R> {
+    #[inline]
+    pub fn selects_server(&self, name: &str) -> bool {
+        self.parents
+            .iter()
+            .any(|p| matches!(p, ParentRef::Server(n) if n == name))
+    }
+
+    #[inline]
+    pub fn accepted_by_server(&self, name: &str) -> bool {
+        self.statuses.iter().any(|status| {
+            status.parent == ParentRef::Server(name.to_string())
+                && status
+                    .conditions
+                    .iter()
+                    .any(|condition| condition.type_ == ConditionType::Accepted && condition.status)
+        })
+    }
+}
+
+impl ParentRef {
+    pub(crate) fn collect_from(
+        route_ns: Option<&str>,
+        parent_refs: Option<Vec<gateway::ParentReference>>,
+    ) -> Result<Vec<Self>, InvalidParentRef> {
+        let parents = parent_refs
+            .into_iter()
+            .flatten()
+            .filter_map(|parent_ref| Self::from_parent_ref(route_ns, parent_ref))
+            .collect::<Result<Vec<_>, InvalidParentRef>>()?;
+
+        Ok(parents)
+    }
+
+    fn from_parent_ref(
+        route_ns: Option<&str>,
+        parent_ref: gateway::ParentReference,
+    ) -> Option<Result<Self, InvalidParentRef>> {
+        // Skip parent refs that don't target a `Server` resource.
+        if !policy::httproute::parent_ref_targets_kind::<policy::Server>(&parent_ref)
+            || parent_ref.name.is_empty()
+        {
+            return None;
+        }
+
+        let gateway::ParentReference {
+            group: _,
+            kind: _,
+            namespace,
+            name,
+            section_name,
+            port,
+        } = parent_ref;
+
+        if namespace.is_some() && namespace.as_deref() != route_ns {
+            return Some(Err(InvalidParentRef::ServerInAnotherNamespace));
+        }
+        if port.is_some() {
+            return Some(Err(InvalidParentRef::SpecifiesPort));
+        }
+        if section_name.is_some() {
+            return Some(Err(InvalidParentRef::SpecifiesSection));
+        }
+
+        Some(Ok(ParentRef::Server(name)))
+    }
+}
+
+impl Status {
+    pub fn collect_from(status: gateway::RouteStatus) -> Vec<Self> {
+        status
+            .parents
+            .iter()
+            .filter(|status| status.controller_name == POLICY_CONTROLLER_NAME)
+            .filter_map(Self::from_parent_status)
+            .collect::<Vec<_>>()
+    }
+
+    fn from_parent_status(status: &gateway::RouteParentStatus) -> Option<Self> {
+        // Only match parent statuses that belong to resources of
+        // `kind: Server`.
+        match status.parent_ref.kind.as_deref() {
+            Some("Server") => (),
+            _ => return None,
+        }
+
+        let conditions = status
+            .conditions
+            .iter()
+            .filter_map(|condition| {
+                let type_ = match condition.type_.as_ref() {
+                    "Accepted" => ConditionType::Accepted,
+                    condition_type => {
+                        tracing::error!(%status.parent_ref.name, %condition_type, "Unexpected condition type found in parent status");
+                        return None;
+                    }
+                };
+                let status = match condition.status.as_ref() {
+                    "True" => true,
+                    "False" => false,
+                    condition_status => {
+                        tracing::error!(%status.parent_ref.name, %type_, %condition_status, "Unexpected condition status found in parent status");
+                        return None
+                    },
+                };
+                Some(Condition { type_, status })
+            })
+            .collect();
+
+        Some(Status {
+            parent: ParentRef::Server(status.parent_ref.name.to_string()),
+            conditions,
+        })
+    }
+}
+
+impl fmt::Display for ConditionType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Accepted => write!(f, "Accepted"),
+        }
+    }
+}
diff --git a/policy-controller/k8s/status/Cargo.toml b/policy-controller/k8s/status/Cargo.toml
index 605ad246e..afe7877c0 100644
--- a/policy-controller/k8s/status/Cargo.toml
+++ b/policy-controller/k8s/status/Cargo.toml
@@ -19,7 +19,7 @@ linkerd-policy-controller-k8s-api = { path = "../api" }
 parking_lot = "0.12"
 prometheus-client = { version = "0.22.0", default-features = false }
 serde = "1"
-serde_json = "1.0.118"
+serde_json = "1.0.119"
 thiserror = "1"
 tokio = { version = "1", features = ["macros"] }
 tracing = "0.1.40"
diff --git a/policy-controller/k8s/status/src/index.rs b/policy-controller/k8s/status/src/index.rs
index 1e714bc1a..eb6f656f7 100644
--- a/policy-controller/k8s/status/src/index.rs
+++ b/policy-controller/k8s/status/src/index.rs
@@ -30,12 +30,12 @@ mod conditions {
     pub const RESOLVED_REFS: &str = "ResolvedRefs";
     pub const ACCEPTED: &str = "Accepted";
 }
-
 mod reasons {
     pub const RESOLVED_REFS: &str = "ResolvedRefs";
     pub const BACKEND_NOT_FOUND: &str = "BackendNotFound";
     pub const INVALID_KIND: &str = "InvalidKind";
     pub const NO_MATCHING_PARENT: &str = "NoMatchingParent";
+    pub const ROUTE_REASON_CONFLICTED: &str = "RouteReasonConflicted";
 }
 
 mod cond_statuses {
@@ -360,13 +360,24 @@ impl Index {
 
     fn parent_status(
         &self,
+        id: &NamespaceGroupKindName,
         parent_ref: &routes::ParentReference,
         backend_condition: k8s_core_api::Condition,
     ) -> Option<k8s_gateway_api::RouteParentStatus> {
         match parent_ref {
             routes::ParentReference::Server(server) => {
                 let condition = if self.servers.contains(server) {
-                    accepted()
+                    // If this route is an HTTPRoute and there exists a GRPCRoute
+                    // with the same parent, the HTTPRoute should not be accepted
+                    // because it is less specific.
+                    // https://gateway-api.sigs.k8s.io/geps/gep-1426/#route-types
+                    if id.gkn.kind == k8s_gateway_api::HttpRoute::kind(&())
+                        && self.parent_has_grpcroute_children(parent_ref)
+                    {
+                        route_conflicted()
+                    } else {
+                        accepted()
+                    }
                 } else {
                     no_matching_parent()
                 };
@@ -391,7 +402,17 @@ impl Index {
                     .get(service)
                     .map_or(false, |svc| svc.valid_parent_service())
                 {
-                    accepted()
+                    // If this route is an HTTPRoute and there exists a GRPCRoute
+                    // with the same parent, the HTTPRoute should not be accepted
+                    // because it is less specific.
+                    // https://gateway-api.sigs.k8s.io/geps/gep-1426/#route-types
+                    if id.gkn.kind == k8s_gateway_api::HttpRoute::kind(&())
+                        && self.parent_has_grpcroute_children(parent_ref)
+                    {
+                        route_conflicted()
+                    } else {
+                        accepted()
+                    }
                 } else {
                     no_matching_parent()
                 };
@@ -413,6 +434,13 @@ impl Index {
         }
     }
 
+    fn parent_has_grpcroute_children(&self, parent_ref: &routes::ParentReference) -> bool {
+        self.route_refs.iter().any(|(id, route)| {
+            id.gkn.kind == k8s_gateway_api::GrpcRoute::kind(&())
+                && route.parents.contains(parent_ref)
+        })
+    }
+
     fn backend_condition(
         &self,
         backend_refs: &[routes::BackendReference],
@@ -456,7 +484,7 @@ impl Index {
         let parent_statuses = route
             .parents
             .iter()
-            .filter_map(|parent_ref| self.parent_status(parent_ref, backend_condition.clone()));
+            .filter_map(|parent_ref| self.parent_status(id, parent_ref, backend_condition.clone()));
 
         let all_statuses = unowned_statuses.chain(parent_statuses).collect::<Vec<_>>();
 
@@ -785,22 +813,7 @@ impl Index {
             return;
         }
 
-        // Create a patch for the route and send it to the Controller so
-        // that it is applied.
-        if let Some(patch) = self.make_route_patch(&id, &route) {
-            match self.updates.try_send(Update {
-                id: id.clone(),
-                patch,
-            }) {
-                Ok(()) => {
-                    self.metrics.patch_enqueues.inc();
-                }
-                Err(error) => {
-                    self.metrics.patch_channel_full.inc();
-                    tracing::error!(%id.namespace, route = ?id.gkn, %error, "Failed to send route patch");
-                }
-            }
-        }
+        self.reconcile()
     }
 }
 
@@ -848,6 +861,17 @@ fn no_matching_parent() -> k8s_core_api::Condition {
     }
 }
 
+fn route_conflicted() -> k8s_core_api::Condition {
+    k8s_core_api::Condition {
+        last_transition_time: k8s_core_api::Time(now()),
+        message: "".to_string(),
+        observed_generation: None,
+        reason: reasons::ROUTE_REASON_CONFLICTED.to_string(),
+        status: cond_statuses::STATUS_FALSE.to_string(),
+        type_: conditions::ACCEPTED.to_string(),
+    }
+}
+
 fn accepted() -> k8s_core_api::Condition {
     k8s_core_api::Condition {
         last_transition_time: k8s_core_api::Time(now()),
diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs
index ac4d2b2a8..039553820 100644
--- a/policy-controller/src/admission.rs
+++ b/policy-controller/src/admission.rs
@@ -11,7 +11,7 @@ use hyper::{body::Buf, http, Body, Request, Response};
 use k8s_openapi::api::core::v1::{Namespace, ServiceAccount};
 use kube::{core::DynamicObject, Resource, ResourceExt};
 use linkerd_policy_controller_core as core;
-use linkerd_policy_controller_k8s_api::gateway as k8s_gateway_api;
+use linkerd_policy_controller_k8s_api::gateway::{self as k8s_gateway_api};
 use linkerd_policy_controller_k8s_index as index;
 use serde::de::DeserializeOwned;
 use std::task;
diff --git a/web/app/package.json b/web/app/package.json
index 4000ed679..07a2cac77 100644
--- a/web/app/package.json
+++ b/web/app/package.json
@@ -62,9 +62,9 @@
     "eslint": "^8.57.0",
     "eslint-config-airbnb": "^19.0.4",
     "eslint-plugin-import": "^2.29.1",
-    "eslint-plugin-jsx-a11y": "^6.7.1",
+    "eslint-plugin-jsx-a11y": "^6.9.0",
     "eslint-plugin-promise": "^6.2.0",
-    "eslint-plugin-react": "^7.34.2",
+    "eslint-plugin-react": "^7.34.3",
     "eslint-plugin-react-hooks": "^4.6.2",
     "eslint-webpack-plugin": "^4.2.0",
     "file-loader": "^6.2.0",
@@ -78,7 +78,7 @@
     "sinon-stub-promise": "4.0.0",
     "style-loader": "^4.0.0",
     "url-loader": "^4.1.1",
-    "webpack": "^5.92.0",
+    "webpack": "^5.92.1",
     "webpack-bundle-analyzer": "4.10.2",
     "webpack-cli": "5.1.4",
     "webpack-dev-server": "5.0.4"
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index abe918997..f971edbef 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -1099,7 +1099,7 @@
   resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
   integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
 
-"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.24.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.20.13", "@babel/runtime@^7.21.0", "@babel/runtime@^7.24.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
   version "7.24.7"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
   integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
@@ -2515,7 +2515,7 @@ argparse@^2.0.1:
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
   integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
 
-aria-query@^5.1.3:
+aria-query@~5.1.3:
   version "5.1.3"
   resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e"
   integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==
@@ -2543,7 +2543,7 @@ array-flatten@1.1.1:
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
   integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
 
-array-includes@^3.1.5, array-includes@^3.1.6, array-includes@^3.1.7, array-includes@^3.1.8:
+array-includes@^3.1.6, array-includes@^3.1.7, array-includes@^3.1.8:
   version "3.1.8"
   resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d"
   integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==
@@ -2609,7 +2609,7 @@ array.prototype.findlastindex@^1.2.3:
     es-shim-unscopables "^1.0.0"
     get-intrinsic "^1.2.1"
 
-array.prototype.flat@^1.2.3, array.prototype.flat@^1.3.2:
+array.prototype.flat@^1.2.3, array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18"
   integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==
@@ -2619,7 +2619,7 @@ array.prototype.flat@^1.2.3, array.prototype.flat@^1.3.2:
     es-abstract "^1.22.1"
     es-shim-unscopables "^1.0.0"
 
-array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2:
+array.prototype.flatmap@^1.3.2:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527"
   integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==
@@ -2639,15 +2639,15 @@ array.prototype.toreversed@^1.1.2:
     es-abstract "^1.22.1"
     es-shim-unscopables "^1.0.0"
 
-array.prototype.tosorted@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8"
-  integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==
+array.prototype.tosorted@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc"
+  integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==
   dependencies:
-    call-bind "^1.0.5"
+    call-bind "^1.0.7"
     define-properties "^1.2.1"
-    es-abstract "^1.22.3"
-    es-errors "^1.1.0"
+    es-abstract "^1.23.3"
+    es-errors "^1.3.0"
     es-shim-unscopables "^1.0.2"
 
 arraybuffer.prototype.slice@^1.0.2:
@@ -2682,10 +2682,10 @@ assertion-error@^1.1.0:
   resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
   integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
 
-ast-types-flow@^0.0.7:
-  version "0.0.7"
-  resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
-  integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
+ast-types-flow@^0.0.8:
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6"
+  integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==
 
 asynckit@^0.4.0:
   version "0.4.0"
@@ -2709,12 +2709,12 @@ available-typed-arrays@^1.0.6, available-typed-arrays@^1.0.7:
   dependencies:
     possible-typed-array-names "^1.0.0"
 
-axe-core@^4.6.2:
-  version "4.6.2"
-  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.2.tgz#6e566ab2a3d29e415f5115bc0fd2597a5eb3e5e3"
-  integrity sha512-b1WlTV8+XKLj9gZy2DZXgQiyDp9xkkoe2a6U6UbYccScq2wgH/YwCeI2/Jq2mgo0HzQxqJOjWZBLeA/mqsk5Mg==
+axe-core@^4.9.1:
+  version "4.9.1"
+  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.9.1.tgz#fcd0f4496dad09e0c899b44f6c4bb7848da912ae"
+  integrity sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==
 
-axobject-query@^3.1.1:
+axobject-query@~3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1"
   integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==
@@ -4090,6 +4090,58 @@ es-abstract@^1.17.4, es-abstract@^1.18.0, es-abstract@^1.18.0-next.2:
     string.prototype.trimstart "^1.0.4"
     unbox-primitive "^1.0.1"
 
+es-abstract@^1.17.5, es-abstract@^1.23.0, es-abstract@^1.23.2, es-abstract@^1.23.3:
+  version "1.23.3"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0"
+  integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==
+  dependencies:
+    array-buffer-byte-length "^1.0.1"
+    arraybuffer.prototype.slice "^1.0.3"
+    available-typed-arrays "^1.0.7"
+    call-bind "^1.0.7"
+    data-view-buffer "^1.0.1"
+    data-view-byte-length "^1.0.1"
+    data-view-byte-offset "^1.0.0"
+    es-define-property "^1.0.0"
+    es-errors "^1.3.0"
+    es-object-atoms "^1.0.0"
+    es-set-tostringtag "^2.0.3"
+    es-to-primitive "^1.2.1"
+    function.prototype.name "^1.1.6"
+    get-intrinsic "^1.2.4"
+    get-symbol-description "^1.0.2"
+    globalthis "^1.0.3"
+    gopd "^1.0.1"
+    has-property-descriptors "^1.0.2"
+    has-proto "^1.0.3"
+    has-symbols "^1.0.3"
+    hasown "^2.0.2"
+    internal-slot "^1.0.7"
+    is-array-buffer "^3.0.4"
+    is-callable "^1.2.7"
+    is-data-view "^1.0.1"
+    is-negative-zero "^2.0.3"
+    is-regex "^1.1.4"
+    is-shared-array-buffer "^1.0.3"
+    is-string "^1.0.7"
+    is-typed-array "^1.1.13"
+    is-weakref "^1.0.2"
+    object-inspect "^1.13.1"
+    object-keys "^1.1.1"
+    object.assign "^4.1.5"
+    regexp.prototype.flags "^1.5.2"
+    safe-array-concat "^1.1.2"
+    safe-regex-test "^1.0.3"
+    string.prototype.trim "^1.2.9"
+    string.prototype.trimend "^1.0.8"
+    string.prototype.trimstart "^1.0.8"
+    typed-array-buffer "^1.0.2"
+    typed-array-byte-length "^1.0.1"
+    typed-array-byte-offset "^1.0.2"
+    typed-array-length "^1.0.6"
+    unbox-primitive "^1.0.2"
+    which-typed-array "^1.1.15"
+
 es-abstract@^1.19.0:
   version "1.19.1"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3"
@@ -4208,58 +4260,6 @@ es-abstract@^1.22.3:
     unbox-primitive "^1.0.2"
     which-typed-array "^1.1.14"
 
-es-abstract@^1.23.0, es-abstract@^1.23.2, es-abstract@^1.23.3:
-  version "1.23.3"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0"
-  integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==
-  dependencies:
-    array-buffer-byte-length "^1.0.1"
-    arraybuffer.prototype.slice "^1.0.3"
-    available-typed-arrays "^1.0.7"
-    call-bind "^1.0.7"
-    data-view-buffer "^1.0.1"
-    data-view-byte-length "^1.0.1"
-    data-view-byte-offset "^1.0.0"
-    es-define-property "^1.0.0"
-    es-errors "^1.3.0"
-    es-object-atoms "^1.0.0"
-    es-set-tostringtag "^2.0.3"
-    es-to-primitive "^1.2.1"
-    function.prototype.name "^1.1.6"
-    get-intrinsic "^1.2.4"
-    get-symbol-description "^1.0.2"
-    globalthis "^1.0.3"
-    gopd "^1.0.1"
-    has-property-descriptors "^1.0.2"
-    has-proto "^1.0.3"
-    has-symbols "^1.0.3"
-    hasown "^2.0.2"
-    internal-slot "^1.0.7"
-    is-array-buffer "^3.0.4"
-    is-callable "^1.2.7"
-    is-data-view "^1.0.1"
-    is-negative-zero "^2.0.3"
-    is-regex "^1.1.4"
-    is-shared-array-buffer "^1.0.3"
-    is-string "^1.0.7"
-    is-typed-array "^1.1.13"
-    is-weakref "^1.0.2"
-    object-inspect "^1.13.1"
-    object-keys "^1.1.1"
-    object.assign "^4.1.5"
-    regexp.prototype.flags "^1.5.2"
-    safe-array-concat "^1.1.2"
-    safe-regex-test "^1.0.3"
-    string.prototype.trim "^1.2.9"
-    string.prototype.trimend "^1.0.8"
-    string.prototype.trimstart "^1.0.8"
-    typed-array-buffer "^1.0.2"
-    typed-array-byte-length "^1.0.1"
-    typed-array-byte-offset "^1.0.2"
-    typed-array-length "^1.0.6"
-    unbox-primitive "^1.0.2"
-    which-typed-array "^1.1.15"
-
 es-array-method-boxes-properly@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e"
@@ -4272,7 +4272,7 @@ es-define-property@^1.0.0:
   dependencies:
     get-intrinsic "^1.2.4"
 
-es-errors@^1.0.0, es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0:
+es-errors@^1.0.0, es-errors@^1.2.1, es-errors@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
   integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
@@ -4460,27 +4460,27 @@ eslint-plugin-import@^2.29.1:
     semver "^6.3.1"
     tsconfig-paths "^3.15.0"
 
-eslint-plugin-jsx-a11y@^6.7.1:
-  version "6.7.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz#fca5e02d115f48c9a597a6894d5bcec2f7a76976"
-  integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==
+eslint-plugin-jsx-a11y@^6.9.0:
+  version "6.9.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz#67ab8ff460d4d3d6a0b4a570e9c1670a0a8245c8"
+  integrity sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==
   dependencies:
-    "@babel/runtime" "^7.20.7"
-    aria-query "^5.1.3"
-    array-includes "^3.1.6"
-    array.prototype.flatmap "^1.3.1"
-    ast-types-flow "^0.0.7"
-    axe-core "^4.6.2"
-    axobject-query "^3.1.1"
+    aria-query "~5.1.3"
+    array-includes "^3.1.8"
+    array.prototype.flatmap "^1.3.2"
+    ast-types-flow "^0.0.8"
+    axe-core "^4.9.1"
+    axobject-query "~3.1.1"
     damerau-levenshtein "^1.0.8"
     emoji-regex "^9.2.2"
-    has "^1.0.3"
-    jsx-ast-utils "^3.3.3"
-    language-tags "=1.0.5"
+    es-iterator-helpers "^1.0.19"
+    hasown "^2.0.2"
+    jsx-ast-utils "^3.3.5"
+    language-tags "^1.0.9"
     minimatch "^3.1.2"
-    object.entries "^1.1.6"
-    object.fromentries "^2.0.6"
-    semver "^6.3.0"
+    object.fromentries "^2.0.8"
+    safe-regex-test "^1.0.3"
+    string.prototype.includes "^2.0.0"
 
 eslint-plugin-promise@^6.2.0:
   version "6.2.0"
@@ -4492,16 +4492,16 @@ eslint-plugin-react-hooks@^4.6.2:
   resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596"
   integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==
 
-eslint-plugin-react@^7.34.2:
-  version "7.34.2"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz#2780a1a35a51aca379d86d29b9a72adc6bfe6b66"
-  integrity sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==
+eslint-plugin-react@^7.34.3:
+  version "7.34.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz#9965f27bd1250a787b5d4cfcc765e5a5d58dcb7b"
+  integrity sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA==
   dependencies:
     array-includes "^3.1.8"
     array.prototype.findlast "^1.2.5"
     array.prototype.flatmap "^1.3.2"
     array.prototype.toreversed "^1.1.2"
-    array.prototype.tosorted "^1.1.3"
+    array.prototype.tosorted "^1.1.4"
     doctrine "^2.1.0"
     es-iterator-helpers "^1.0.19"
     estraverse "^5.3.0"
@@ -5263,21 +5263,7 @@ has@^1.0.3:
   dependencies:
     function-bind "^1.1.1"
 
-hasown@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
-  integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
-  dependencies:
-    function-bind "^1.1.2"
-
-hasown@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa"
-  integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==
-  dependencies:
-    function-bind "^1.1.2"
-
-hasown@^2.0.2:
+hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
   integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
@@ -6657,13 +6643,15 @@ jss@10.8.0, jss@^10.5.1:
     is-in-browser "^1.1.3"
     tiny-warning "^1.0.2"
 
-"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3:
-  version "3.3.3"
-  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea"
-  integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==
+"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5:
+  version "3.3.5"
+  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a"
+  integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==
   dependencies:
-    array-includes "^3.1.5"
-    object.assign "^4.1.3"
+    array-includes "^3.1.6"
+    array.prototype.flat "^1.3.1"
+    object.assign "^4.1.4"
+    object.values "^1.1.6"
 
 just-extend@^6.2.0:
   version "6.2.0"
@@ -6680,17 +6668,17 @@ kleur@^3.0.3:
   resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
   integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
 
-language-subtag-registry@~0.3.2:
-  version "0.3.21"
-  resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"
-  integrity sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==
+language-subtag-registry@^0.3.20:
+  version "0.3.23"
+  resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7"
+  integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==
 
-language-tags@=1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a"
-  integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==
+language-tags@^1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777"
+  integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==
   dependencies:
-    language-subtag-registry "~0.3.2"
+    language-subtag-registry "^0.3.20"
 
 launch-editor@^2.6.1:
   version "2.6.1"
@@ -7178,7 +7166,7 @@ object-keys@^1.0.12, object-keys@^1.1.1:
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
 
-object.assign@^4.1.0, object.assign@^4.1.2, object.assign@^4.1.3, object.assign@^4.1.4, object.assign@^4.1.5:
+object.assign@^4.1.0, object.assign@^4.1.2, object.assign@^4.1.4, object.assign@^4.1.5:
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0"
   integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==
@@ -7188,7 +7176,7 @@ object.assign@^4.1.0, object.assign@^4.1.2, object.assign@^4.1.3, object.assign@
     has-symbols "^1.0.3"
     object-keys "^1.1.1"
 
-object.entries@^1.1.1, object.entries@^1.1.2, object.entries@^1.1.5, object.entries@^1.1.6, object.entries@^1.1.8:
+object.entries@^1.1.1, object.entries@^1.1.2, object.entries@^1.1.5, object.entries@^1.1.8:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41"
   integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==
@@ -7197,7 +7185,7 @@ object.entries@^1.1.1, object.entries@^1.1.2, object.entries@^1.1.5, object.entr
     define-properties "^1.2.1"
     es-object-atoms "^1.0.0"
 
-object.fromentries@^2.0.6, object.fromentries@^2.0.7, object.fromentries@^2.0.8:
+object.fromentries@^2.0.7, object.fromentries@^2.0.8:
   version "2.0.8"
   resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65"
   integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==
@@ -7226,7 +7214,7 @@ object.hasown@^1.1.4:
     es-abstract "^1.23.2"
     es-object-atoms "^1.0.0"
 
-object.values@^1.1.1, object.values@^1.1.7, object.values@^1.2.0:
+object.values@^1.1.1, object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b"
   integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==
@@ -8312,16 +8300,7 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
-safe-regex-test@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295"
-  integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==
-  dependencies:
-    call-bind "^1.0.2"
-    get-intrinsic "^1.1.3"
-    is-regex "^1.1.4"
-
-safe-regex-test@^1.0.3:
+safe-regex-test@^1.0.0, safe-regex-test@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377"
   integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==
@@ -8744,6 +8723,14 @@ string-width@^5.0.1, string-width@^5.1.2:
     emoji-regex "^9.2.2"
     strip-ansi "^7.0.1"
 
+string.prototype.includes@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz#8986d57aee66d5460c144620a6d873778ad7289f"
+  integrity sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+
 string.prototype.matchall@^4.0.11:
   version "4.0.11"
   resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a"
@@ -9515,10 +9502,10 @@ webpack-sources@^3.2.3:
   resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
   integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
 
-webpack@^5.92.0:
-  version "5.92.0"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.92.0.tgz#cc114c71e6851d220b1feaae90159ed52c876bdf"
-  integrity sha512-Bsw2X39MYIgxouNATyVpCNVWBCuUwDgWtN78g6lSdPJRLaQ/PUVm/oXcaRAyY/sMFoKFQrsPeqvTizWtq7QPCA==
+webpack@^5.92.1:
+  version "5.92.1"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.92.1.tgz#eca5c1725b9e189cffbd86e8b6c3c7400efc5788"
+  integrity sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==
   dependencies:
     "@types/eslint-scope" "^3.7.3"
     "@types/estree" "^1.0.5"


Test patch:
diff --git a/cli/cmd/testdata/install_crds.golden b/cli/cmd/testdata/install_crds.golden
index 4a9d6059f..a9d0ffb06 100644
--- a/cli/cmd/testdata/install_crds.golden
+++ b/cli/cmd/testdata/install_crds.golden
@@ -11732,7 +11732,6 @@ spec:
     listKind: ExternalWorkloadList
     plural: externalworkloads
     singular: externalworkload
-    shortNames: []
   scope: Namespaced
   versions:
   - name: v1alpha1
diff --git a/cli/cmd/testdata/install_helm_crds_output.golden b/cli/cmd/testdata/install_helm_crds_output.golden
index 8b9a358b3..5f0a2d89a 100644
--- a/cli/cmd/testdata/install_helm_crds_output.golden
+++ b/cli/cmd/testdata/install_helm_crds_output.golden
@@ -11752,7 +11752,6 @@ spec:
     listKind: ExternalWorkloadList
     plural: externalworkloads
     singular: externalworkload
-    shortNames: []
   scope: Namespaced
   versions:
   - name: v1alpha1
diff --git a/cli/cmd/testdata/install_helm_crds_output_ha.golden b/cli/cmd/testdata/install_helm_crds_output_ha.golden
index 8b9a358b3..5f0a2d89a 100644
--- a/cli/cmd/testdata/install_helm_crds_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_crds_output_ha.golden
@@ -11752,7 +11752,6 @@ spec:
     listKind: ExternalWorkloadList
     plural: externalworkloads
     singular: externalworkload
-    shortNames: []
   scope: Namespaced
   versions:
   - name: v1alpha1
diff --git a/policy-controller/k8s/index/src/inbound/tests.rs b/policy-controller/k8s/index/src/inbound/tests.rs
index fec468d5d..a14d31cdc 100644
--- a/policy-controller/k8s/index/src/inbound/tests.rs
+++ b/policy-controller/k8s/index/src/inbound/tests.rs
@@ -13,8 +13,8 @@ use ahash::AHashMap as HashMap;
 use kubert::index::IndexNamespacedResource;
 use linkerd_policy_controller_core::{
     inbound::{
-        AuthorizationRef, ClientAuthentication, ClientAuthorization, HttpRoute, HttpRouteRef,
-        InboundServer, ProxyProtocol, ServerRef,
+        AuthorizationRef, ClientAuthentication, ClientAuthorization, HttpRoute, InboundServer,
+        ProxyProtocol, RouteRef, ServerRef,
     },
     IdentityMatch, IpNet, Ipv4Net, Ipv6Net, NetworkMatch,
 };
@@ -181,8 +181,8 @@ fn mk_default_policy(
     .collect()
 }
 
-fn mk_default_routes() -> HashMap<HttpRouteRef, HttpRoute> {
-    Some((HttpRouteRef::Default("default"), HttpRoute::default()))
+fn mk_default_http_routes() -> HashMap<RouteRef, HttpRoute> {
+    Some((RouteRef::Default("default"), HttpRoute::default()))
         .into_iter()
         .collect()
 }
@@ -226,7 +226,7 @@ impl TestConfig {
             protocol: ProxyProtocol::Detect {
                 timeout: self.detect_timeout,
             },
-            http_routes: mk_default_routes(),
+            http_routes: mk_default_http_routes(),
         }
     }
 
diff --git a/policy-controller/k8s/index/src/inbound/tests/annotation.rs b/policy-controller/k8s/index/src/inbound/tests/annotation.rs
index 71efbc50e..ec26eb59e 100644
--- a/policy-controller/k8s/index/src/inbound/tests/annotation.rs
+++ b/policy-controller/k8s/index/src/inbound/tests/annotation.rs
@@ -116,7 +116,7 @@ fn authenticated_annotated() {
                 protocol: ProxyProtocol::Detect {
                     timeout: test.detect_timeout,
                 },
-                http_routes: mk_default_routes(),
+                http_routes: mk_default_http_routes(),
             }
         };
 
diff --git a/policy-controller/k8s/index/src/inbound/tests/authorization_policy.rs b/policy-controller/k8s/index/src/inbound/tests/authorization_policy.rs
index 00c289ea6..018ab4aaa 100644
--- a/policy-controller/k8s/index/src/inbound/tests/authorization_policy.rs
+++ b/policy-controller/k8s/index/src/inbound/tests/authorization_policy.rs
@@ -34,7 +34,7 @@ fn links_authorization_policy_with_mtls_name() {
             reference: ServerRef::Server("srv-8080".to_string()),
             authorizations: Default::default(),
             protocol: ProxyProtocol::Http1,
-            http_routes: mk_default_routes(),
+            http_routes: mk_default_http_routes(),
         },
     );
 
@@ -88,7 +88,7 @@ fn links_authorization_policy_with_mtls_name() {
             .into_iter()
             .collect(),
             protocol: ProxyProtocol::Http1,
-            http_routes: mk_default_routes(),
+            http_routes: mk_default_http_routes(),
         },
     );
 }
@@ -124,7 +124,7 @@ fn authorization_targets_namespace() {
             reference: ServerRef::Server("srv-8080".to_string()),
             authorizations: Default::default(),
             protocol: ProxyProtocol::Http1,
-            http_routes: mk_default_routes(),
+            http_routes: mk_default_http_routes(),
         },
     );
 
@@ -178,7 +178,7 @@ fn authorization_targets_namespace() {
             .into_iter()
             .collect(),
             protocol: ProxyProtocol::Http1,
-            http_routes: mk_default_routes(),
+            http_routes: mk_default_http_routes(),
         },
     );
 }
@@ -214,7 +214,7 @@ fn links_authorization_policy_with_service_account() {
             reference: ServerRef::Server("srv-8080".to_string()),
             authorizations: Default::default(),
             protocol: ProxyProtocol::Http1,
-            http_routes: mk_default_routes(),
+            http_routes: mk_default_http_routes(),
         },
     );
 
@@ -262,7 +262,7 @@ fn links_authorization_policy_with_service_account() {
             .into_iter()
             .collect(),
             protocol: ProxyProtocol::Http1,
-            http_routes: mk_default_routes(),
+            http_routes: mk_default_http_routes(),
         },
     );
 }
@@ -363,12 +363,12 @@ fn authorization_policy_prevents_index_deletion() {
             reference: ServerRef::Server("srv-8080".to_string()),
             authorizations: Default::default(),
             protocol: ProxyProtocol::Http1,
-            http_routes: hashmap!(HttpRouteRef::Linkerd(routes::GroupKindName{
+            http_routes: hashmap!(RouteRef::Resource(routes::GroupKindName{
                 group: "policy.linkerd.io".into(),
                 kind: "HTTPRoute".into(),
                 name: "route-foo".into(),
             }) => HttpRoute {
-                rules: vec![inbound::HttpRouteRule {
+                rules: vec![inbound::InboundRouteRule {
                     matches: vec![routes::HttpRouteMatch {
                         path: Some(routes::PathMatch::Prefix("/foo".to_string())),
                         headers: vec![],
diff --git a/policy-controller/k8s/index/src/inbound/tests/http_routes.rs b/policy-controller/k8s/index/src/inbound/tests/http_routes.rs
index f34b92af4..a508458f2 100644
--- a/policy-controller/k8s/index/src/inbound/tests/http_routes.rs
+++ b/policy-controller/k8s/index/src/inbound/tests/http_routes.rs
@@ -40,7 +40,7 @@ fn route_attaches_to_server() {
             reference: ServerRef::Server("srv-8080".to_string()),
             authorizations: Default::default(),
             protocol: ProxyProtocol::Http1,
-            http_routes: mk_default_routes(),
+            http_routes: mk_default_http_routes(),
         },
     );
 
@@ -56,9 +56,7 @@ fn route_attaches_to_server() {
     assert!(rx
         .borrow_and_update()
         .http_routes
-        .contains_key(&HttpRouteRef::Linkerd(
-            "route-foo".gkn::<policy::HttpRoute>()
-        )));
+        .contains_key(&RouteRef::Resource("route-foo".gkn::<policy::HttpRoute>())));
 
     // Create authz policy.
     test.index.write().apply(mk_authorization_policy(
@@ -74,12 +72,13 @@ fn route_attaches_to_server() {
     ));
 
     assert!(rx.has_changed().unwrap());
-    assert!(rx.borrow().http_routes
-        [&HttpRouteRef::Linkerd("route-foo".gkn::<policy::HttpRoute>())]
-        .authorizations
-        .contains_key(&AuthorizationRef::AuthorizationPolicy(
-            "authz-foo".to_string()
-        )));
+    assert!(
+        rx.borrow().http_routes[&RouteRef::Resource("route-foo".gkn::<policy::HttpRoute>())]
+            .authorizations
+            .contains_key(&AuthorizationRef::AuthorizationPolicy(
+                "authz-foo".to_string()
+            ))
+    );
 }
 
 #[test]
@@ -146,10 +145,7 @@ fn routes_created_for_probes() {
     // No Server is configured for the port, so expect the probe paths to be
     // authorized.
     let update = rx.borrow_and_update();
-    let probes = update
-        .http_routes
-        .get(&HttpRouteRef::Default("probe"))
-        .unwrap();
+    let probes = update.http_routes.get(&RouteRef::Default("probe")).unwrap();
     let probes_rules = probes.rules.first().unwrap();
     assert!(
         probes_rules.matches.contains(&liveness_match),
@@ -178,10 +174,7 @@ fn routes_created_for_probes() {
     // // No routes are configured for the Server, so we should still expect the
     // // Pod's probe paths to be authorized.
     let update = rx.borrow_and_update();
-    let probes = update
-        .http_routes
-        .get(&HttpRouteRef::Default("probe"))
-        .unwrap();
+    let probes = update.http_routes.get(&RouteRef::Default("probe")).unwrap();
     let probes_rules = probes.rules.first().unwrap();
     assert!(
         probes_rules.matches.contains(&liveness_match),
@@ -207,7 +200,7 @@ fn routes_created_for_probes() {
     assert!(!rx
         .borrow_and_update()
         .http_routes
-        .contains_key(&HttpRouteRef::Default("probes")));
+        .contains_key(&RouteRef::Default("probes")));
 }
 
 fn mk_route(
diff --git a/policy-controller/k8s/index/src/inbound/tests/server_authorization.rs b/policy-controller/k8s/index/src/inbound/tests/server_authorization.rs
index 455f00a6f..b50ee5230 100644
--- a/policy-controller/k8s/index/src/inbound/tests/server_authorization.rs
+++ b/policy-controller/k8s/index/src/inbound/tests/server_authorization.rs
@@ -43,7 +43,7 @@ fn link_server_authz(selector: ServerSelector) {
             reference: ServerRef::Server("srv-8080".to_string()),
             authorizations: Default::default(),
             protocol: ProxyProtocol::Http1,
-            http_routes: mk_default_routes(),
+            http_routes: mk_default_http_routes(),
         },
     );
     test.index.write().apply(mk_server_authz(
diff --git a/policy-controller/k8s/status/src/tests/routes/grpc.rs b/policy-controller/k8s/status/src/tests/routes/grpc.rs
index 006e1862d..af578ab41 100644
--- a/policy-controller/k8s/status/src/tests/routes/grpc.rs
+++ b/policy-controller/k8s/status/src/tests/routes/grpc.rs
@@ -457,6 +457,139 @@ fn route_rejected_after_server_delete() {
     assert!(updates_rx.try_recv().is_err());
 }
 
+#[test]
+fn service_route_type_conflict() {
+    let hostname = "test";
+    let claim = kubert::lease::Claim {
+        holder: "test".to_string(),
+        expiry: DateTime::<Utc>::MAX_UTC,
+    };
+    let (_claims_tx, claims_rx) = watch::channel(Arc::new(claim));
+    let (updates_tx, mut updates_rx) = mpsc::channel(10000);
+    let index = Index::shared(
+        hostname,
+        claims_rx,
+        updates_tx,
+        IndexMetrics::register(&mut Default::default()),
+    );
+
+    // Apply the parent service
+    let parent = super::make_service("ns-0", "svc");
+    index.write().apply(parent.clone());
+
+    let parent = k8s_gateway_api::ParentReference {
+        group: Some("core".to_string()),
+        kind: Some("Service".to_string()),
+        namespace: parent.namespace(),
+        name: parent.name_unchecked(),
+        section_name: None,
+        port: Some(8080),
+    };
+
+    // Apply the HTTP route.
+    let http_id = NamespaceGroupKindName {
+        namespace: parent.namespace.as_deref().unwrap().to_string(),
+        gkn: GroupKindName {
+            group: k8s_gateway_api::HttpRoute::group(&()),
+            kind: k8s_gateway_api::HttpRoute::kind(&()),
+            name: "httproute-foo".into(),
+        },
+    };
+    let http_route = k8s_gateway_api::HttpRoute {
+        status: None,
+        metadata: k8s_core_api::ObjectMeta {
+            name: Some(http_id.gkn.name.to_string()),
+            namespace: Some(http_id.namespace.clone()),
+            creation_timestamp: Some(k8s_core_api::Time(Utc::now())),
+            ..Default::default()
+        },
+        spec: k8s_gateway_api::HttpRouteSpec {
+            inner: k8s_gateway_api::CommonRouteSpec {
+                parent_refs: Some(vec![parent.clone()]),
+            },
+            hostnames: None,
+            rules: Some(vec![]),
+        },
+    };
+    index.write().apply(http_route);
+
+    // Create the expected update -- HTTPRoute should be accepted
+    let accepted_condition = k8s_core_api::Condition {
+        last_transition_time: k8s_core_api::Time(DateTime::<Utc>::MIN_UTC),
+        message: "".to_string(),
+        observed_generation: None,
+        reason: "Accepted".to_string(),
+        status: "True".to_string(),
+        type_: "Accepted".to_string(),
+    };
+    // No backends were specified, so we have vacuously resolved them all.
+    let backend_condition = k8s_core_api::Condition {
+        last_transition_time: k8s_core_api::Time(DateTime::<Utc>::MIN_UTC),
+        message: "".to_string(),
+        observed_generation: None,
+        reason: "ResolvedRefs".to_string(),
+        status: "True".to_string(),
+        type_: "ResolvedRefs".to_string(),
+    };
+    let parent_status = k8s_gateway_api::RouteParentStatus {
+        parent_ref: parent.clone(),
+        controller_name: POLICY_CONTROLLER_NAME.to_string(),
+        conditions: vec![accepted_condition.clone(), backend_condition.clone()],
+    };
+    let status = make_status(vec![parent_status]);
+    let patch = crate::index::make_patch(&http_id, status).unwrap();
+    let update = updates_rx.try_recv().unwrap();
+    assert_eq!(http_id, update.id);
+    assert_eq!(patch, update.patch);
+
+    // Apply the GRPC route.
+    let grpc_id = NamespaceGroupKindName {
+        namespace: parent.namespace.as_deref().unwrap().to_string(),
+        gkn: GroupKindName {
+            group: k8s_gateway_api::GrpcRoute::group(&()),
+            kind: k8s_gateway_api::GrpcRoute::kind(&()),
+            name: "grpcroute-foo".into(),
+        },
+    };
+    let route = make_route(&grpc_id, parent.clone(), None);
+    index.write().apply(route);
+
+    // Two expected updates: HTTPRoute should be rejected and GRPCRoute should be accepted
+    for _ in 0..2 {
+        let update = updates_rx.try_recv().unwrap();
+        if update.id.gkn.kind == k8s_gateway_api::HttpRoute::kind(&()) {
+            let conflict_condition = k8s_core_api::Condition {
+                last_transition_time: k8s_core_api::Time(DateTime::<Utc>::MIN_UTC),
+                message: "".to_string(),
+                observed_generation: None,
+                reason: "RouteReasonConflicted".to_string(),
+                status: "False".to_string(),
+                type_: "Accepted".to_string(),
+            };
+            let parent_status = k8s_gateway_api::RouteParentStatus {
+                parent_ref: parent.clone(),
+                controller_name: POLICY_CONTROLLER_NAME.to_string(),
+                conditions: vec![conflict_condition, backend_condition.clone()],
+            };
+            let status = make_status(vec![parent_status]);
+            let patch = crate::index::make_patch(&http_id, status).unwrap();
+            assert_eq!(patch, update.patch);
+        } else {
+            let parent_status = k8s_gateway_api::RouteParentStatus {
+                parent_ref: parent.clone(),
+                controller_name: POLICY_CONTROLLER_NAME.to_string(),
+                conditions: vec![accepted_condition.clone(), backend_condition.clone()],
+            };
+            let status = make_status(vec![parent_status]);
+            let patch = crate::index::make_patch(&grpc_id, status).unwrap();
+            assert_eq!(patch, update.patch);
+        }
+    }
+
+    // No more updates.
+    assert!(updates_rx.try_recv().is_err())
+}
+
 fn make_route(
     id: &NamespaceGroupKindName,
     parent: k8s_gateway_api::ParentReference,