Repository: linkerd/linkerd2
Pull request #12748 (2024-06-19 00:29:15)

Manage GrpcRoute resource status
We add support for GrpcRoute resources in the policy-controller's status controller.  This means that the policy controller will watch GrpcRoute resources in the cluster and keep their status up to date, in the same way that it currently does for HttpRoute resources.

<!--  Thanks for sending a pull request!

If you already have a well-structured git commit message, chances are GitHub
set the title and description of this PR to the git commit message subject and
body, respectively. If so, you may delete these instructions and submit your PR.

If this is your first time, please read our contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md

The title and description of your Pull Request should match the git commit
subject and body, respectively. Git commit messages are structured as follows:

```
Subject

Problem

Solution

Validation

Fixes #[GitHub issue ID]

DCO Sign off
```

Example git commit message:

```
Introduce Pull Request Template

GitHub's community guidelines recommend a pull request template, the repo was
lacking one.

Introduce a `PULL_REQUEST_TEMPLATE.md` file.

Once merged, the
[Community profile checklist](https://github.com/linkerd/linkerd2/community)
should indicate the repo now provides a pull request template.

Fixes #3321

Signed-off-by: Jane Smith <jane.smith@example.com>
```

Note the git commit message subject becomes the pull request title.

For more details around git commits, see the section on Committing in our
contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md#committing
-->



Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 329aedd7e..ae9655576 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
-      - uses: tj-actions/changed-files@d6babd6899969df1a11d14c368283ea4436bca78
+      - uses: tj-actions/changed-files@eaf854ef0c266753e1abec356dcf17d92695b251
         id: changed
         with:
           files: |
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 67f0b0e6b..51cc18a0c 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -28,7 +28,7 @@ jobs:
       - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
       - id: tag
         run: echo "tag=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> "$GITHUB_OUTPUT"
-      - uses: tj-actions/changed-files@d6babd6899969df1a11d14c368283ea4436bca78
+      - uses: tj-actions/changed-files@eaf854ef0c266753e1abec356dcf17d92695b251
         id: core
         with:
           files: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ecf620a87..6d97a3b62 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -199,7 +199,7 @@ jobs:
         #  mv choco/linkerd.*.nupkg "target/release/linkerd2-cli-stable-$v.nupkg" || true
       - name: Create release
         id: create_release
-        uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87
+        uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0
         with:
           name: "${{ needs.tag.outputs.tag }}"
           generate_release_notes: true
diff --git a/.proxy-version b/.proxy-version
index 371db4ec0..e6c040c7b 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.236.0
+v2.237.0
diff --git a/Cargo.lock b/Cargo.lock
index 0a755819d..8c5b7f2a3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -421,17 +421,6 @@ dependencies = [
  "crypto-common",
 ]
 
-[[package]]
-name = "displaydoc"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.66",
-]
-
 [[package]]
 name = "drain"
 version = "0.1.2"
@@ -825,124 +814,6 @@ dependencies = [
  "cc",
 ]
 
-[[package]]
-name = "icu_collections"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
-dependencies = [
- "displaydoc",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
-dependencies = [
- "displaydoc",
- "litemap",
- "tinystr",
- "writeable",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_locid_transform_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
-
-[[package]]
-name = "icu_normalizer"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
-dependencies = [
- "displaydoc",
- "icu_collections",
- "icu_normalizer_data",
- "icu_properties",
- "icu_provider",
- "smallvec",
- "utf16_iter",
- "utf8_iter",
- "write16",
- "zerovec",
-]
-
-[[package]]
-name = "icu_normalizer_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
-
-[[package]]
-name = "icu_properties"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036"
-dependencies = [
- "displaydoc",
- "icu_collections",
- "icu_locid_transform",
- "icu_properties_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_properties_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
-
-[[package]]
-name = "icu_provider"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_provider_macros",
- "stable_deref_trait",
- "tinystr",
- "writeable",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_provider_macros"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.66",
-]
-
 [[package]]
 name = "ident_case"
 version = "1.0.1"
@@ -951,14 +822,12 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
 [[package]]
 name = "idna"
-version = "1.0.0"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
 dependencies = [
- "icu_normalizer",
- "icu_properties",
- "smallvec",
- "utf8_iter",
+ "unicode-bidi",
+ "unicode-normalization",
 ]
 
 [[package]]
@@ -1070,9 +939,9 @@ dependencies = [
 
 [[package]]
 name = "k8s-gateway-api"
-version = "0.15.0"
+version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ae4cd917edb1eba13a1413b46b98d292fd10919d86a573311a564ee4f208f44"
+checksum = "84b31e055e9626a2feb83cea00e22f796392edbe634fd4bafc0c73bf6d035ec0"
 dependencies = [
  "k8s-openapi",
  "kube",
@@ -1306,7 +1175,6 @@ dependencies = [
  "hyper",
  "ipnet",
  "jemallocator",
- "k8s-gateway-api",
  "k8s-openapi",
  "kube",
  "kubert",
@@ -1472,12 +1340,6 @@ version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
-[[package]]
-name = "litemap"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
-
 [[package]]
 name = "lock_api"
 version = "0.4.12"
@@ -1790,9 +1652,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.85"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
 dependencies = [
  "unicode-ident",
 ]
@@ -2274,12 +2136,6 @@ version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
 
-[[package]]
-name = "stable_deref_trait"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
-
 [[package]]
 name = "strsim"
 version = "0.11.1"
@@ -2314,17 +2170,6 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
 
-[[package]]
-name = "synstructure"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.66",
-]
-
 [[package]]
 name = "thiserror"
 version = "1.0.61"
@@ -2356,15 +2201,20 @@ dependencies = [
 ]
 
 [[package]]
-name = "tinystr"
-version = "0.7.6"
+name = "tinyvec"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
 dependencies = [
- "displaydoc",
- "zerovec",
+ "tinyvec_macros",
 ]
 
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
 [[package]]
 name = "tokio"
 version = "1.38.0"
@@ -2682,12 +2532,27 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
 
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
 [[package]]
 name = "unsafe-libyaml"
 version = "0.2.11"
@@ -2702,9 +2567,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
 
 [[package]]
 name = "url"
-version = "2.5.1"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
 dependencies = [
  "form_urlencoded",
  "idna",
@@ -2717,18 +2582,6 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
-[[package]]
-name = "utf16_iter"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
-
-[[package]]
-name = "utf8_iter"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
-
 [[package]]
 name = "valuable"
 version = "0.1.0"
@@ -2986,42 +2839,6 @@ version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
 
-[[package]]
-name = "write16"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
-
-[[package]]
-name = "writeable"
-version = "0.5.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
-
-[[package]]
-name = "yoke"
-version = "0.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
-dependencies = [
- "serde",
- "stable_deref_trait",
- "yoke-derive",
- "zerofrom",
-]
-
-[[package]]
-name = "yoke-derive"
-version = "0.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.66",
- "synstructure",
-]
-
 [[package]]
 name = "zerocopy"
 version = "0.7.34"
@@ -3042,51 +2859,8 @@ dependencies = [
  "syn 2.0.66",
 ]
 
-[[package]]
-name = "zerofrom"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
-dependencies = [
- "zerofrom-derive",
-]
-
-[[package]]
-name = "zerofrom-derive"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.66",
- "synstructure",
-]
-
 [[package]]
 name = "zeroize"
 version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
-
-[[package]]
-name = "zerovec"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c"
-dependencies = [
- "yoke",
- "zerofrom",
- "zerovec-derive",
-]
-
-[[package]]
-name = "zerovec-derive"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.66",
-]
diff --git a/charts/linkerd-control-plane/templates/destination-rbac.yaml b/charts/linkerd-control-plane/templates/destination-rbac.yaml
index d05ed0dd3..38488cd04 100644
--- a/charts/linkerd-control-plane/templates/destination-rbac.yaml
+++ b/charts/linkerd-control-plane/templates/destination-rbac.yaml
@@ -190,6 +190,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -233,6 +234,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -247,6 +249,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
diff --git a/go.mod b/go.mod
index 83ac1ac7e..726a3a368 100644
--- a/go.mod
+++ b/go.mod
@@ -48,7 +48,7 @@ require (
 	k8s.io/client-go v0.30.2
 	k8s.io/code-generator v0.30.2
 	k8s.io/endpointslice v0.30.2
-	k8s.io/klog/v2 v2.130.0
+	k8s.io/klog/v2 v2.130.1
 	k8s.io/kube-aggregator v0.30.2
 	k8s.io/utils v0.0.0-20230726121419-3b25d923346b
 	sigs.k8s.io/gateway-api v0.8.1
diff --git a/go.sum b/go.sum
index f26847343..2ef306201 100644
--- a/go.sum
+++ b/go.sum
@@ -760,8 +760,8 @@ k8s.io/endpointslice v0.30.2 h1:d11My11frxpBD+55vx1lHeO2JlH9YQS8Xqt7EInvPLA=
 k8s.io/endpointslice v0.30.2/go.mod h1:DGJe7hJrcKnuuIT8XljHj5yc5AcaA6kCCo85m2JWzdg=
 k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo=
 k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8=
-k8s.io/klog/v2 v2.130.0 h1:5nB3+3HpqKqXJIXNtJdtxcDCfaa9KL8StJgMzGJkUkM=
-k8s.io/klog/v2 v2.130.0/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-aggregator v0.30.2 h1:0+yk/ED6foCprY8VmkDPUhngjaAPKsNTXB/UrtvbIz0=
 k8s.io/kube-aggregator v0.30.2/go.mod h1:EhqCfDdxysNWXk1wRL9SEHAdo1DKl6EULQagztkBcXE=
 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
diff --git a/policy-controller/Cargo.toml b/policy-controller/Cargo.toml
index 5a67c6017..337bd4c64 100644
--- a/policy-controller/Cargo.toml
+++ b/policy-controller/Cargo.toml
@@ -17,7 +17,6 @@ anyhow = "1"
 async-trait = "0.1"
 drain = "0.1"
 futures = { version = "0.3", default-features = false }
-k8s-gateway-api = "0.15"
 k8s-openapi = { version = "0.20", features = ["v1_22"] }
 hyper = { version = "0.14", features = ["http1", "http2", "runtime", "server"] }
 ipnet = { version = "2", default-features = false }
diff --git a/policy-controller/k8s/api/Cargo.toml b/policy-controller/k8s/api/Cargo.toml
index 3c0b07a5b..ea44a5039 100644
--- a/policy-controller/k8s/api/Cargo.toml
+++ b/policy-controller/k8s/api/Cargo.toml
@@ -9,7 +9,7 @@ publish = false
 k8s-openapi = { version = "0.20", default-features = false, features = [
     "v1_22",
 ] }
-k8s-gateway-api = "0.15"
+k8s-gateway-api = { version = "0.16", features = ["experimental"] }
 kube = { version = "0.87.1", default-features = false, features = [
     "client",
     "derive",
diff --git a/policy-controller/k8s/status/src/index.rs b/policy-controller/k8s/status/src/index.rs
index 29d146aff..1e714bc1a 100644
--- a/policy-controller/k8s/status/src/index.rs
+++ b/policy-controller/k8s/status/src/index.rs
@@ -236,6 +236,8 @@ impl Controller {
                             self.patch_status::<linkerd_k8s_api::HttpRoute>(&id.gkn.name, &id.namespace, patch).await;
                         } else if id.gkn.group == k8s_gateway_api::HttpRoute::group(&()) && id.gkn.kind == k8s_gateway_api::HttpRoute::kind(&()) {
                             self.patch_status::<k8s_gateway_api::HttpRoute>(&id.gkn.name, &id.namespace, patch).await;
+                        } else if id.gkn.group == k8s_gateway_api::GrpcRoute::group(&()) && id.gkn.kind == k8s_gateway_api::GrpcRoute::kind(&()) {
+                            self.patch_status::<k8s_gateway_api::GrpcRoute>(&id.gkn.name, &id.namespace, patch).await;
                         }
                     } else {
                         self.metrics.patch_drops.inc();
@@ -483,6 +485,15 @@ impl Index {
 
                 make_patch(id, status)
             }
+            (GATEWAY_API_GROUP, "GRPCRoute") => {
+                let status = k8s_gateway_api::GrpcRouteStatus {
+                    inner: k8s_gateway_api::RouteStatus {
+                        parents: all_statuses,
+                    },
+                };
+
+                make_patch(id, status)
+            }
             _ => None,
         }
     }
@@ -631,6 +642,68 @@ impl kubert::index::IndexNamespacedResource<k8s_gateway_api::HttpRoute> for Inde
     // to handle resets specially.
 }
 
+impl kubert::index::IndexNamespacedResource<k8s_gateway_api::GrpcRoute> for Index {
+    fn apply(&mut self, resource: k8s_gateway_api::GrpcRoute) {
+        let namespace = resource
+            .namespace()
+            .expect("GRPCRoute must have a namespace");
+        let name = resource.name_unchecked();
+        let id = NamespaceGroupKindName {
+            namespace: namespace.clone(),
+            gkn: GroupKindName {
+                name: name.into(),
+                kind: k8s_gateway_api::GrpcRoute::kind(&()),
+                group: k8s_gateway_api::GrpcRoute::group(&()),
+            },
+        };
+
+        // Create the route parents
+        let parents = routes::http::make_parents(&namespace, &resource.spec.inner);
+
+        // Create the route backends
+        let backends = routes::grpc::make_backends(
+            &namespace,
+            resource
+                .spec
+                .rules
+                .into_iter()
+                .flatten()
+                .flat_map(|rule| rule.backend_refs)
+                .flatten(),
+        );
+
+        let statuses = resource
+            .status
+            .into_iter()
+            .flat_map(|status| status.inner.parents)
+            .collect();
+
+        // Construct route and insert into the index; if the GRPCRoute is
+        // already in the index and it hasn't changed, skip creating a patch.
+        let route = RouteRef {
+            parents,
+            backends,
+            statuses,
+        };
+        self.index_route(id, route);
+    }
+
+    fn delete(&mut self, namespace: String, name: String) {
+        let id = NamespaceGroupKindName {
+            namespace,
+            gkn: GroupKindName {
+                name: name.into(),
+                kind: k8s_gateway_api::GrpcRoute::kind(&()),
+                group: k8s_gateway_api::GrpcRoute::group(&()),
+            },
+        };
+        self.route_refs.remove(&id);
+    }
+
+    // Since apply only reindexes a single GRPCRoute at a time, there's no need
+    // to handle resets specially.
+}
+
 impl kubert::index::IndexNamespacedResource<linkerd_k8s_api::Server> for Index {
     fn apply(&mut self, resource: linkerd_k8s_api::Server) {
         let namespace = resource.namespace().expect("Server must have a namespace");
diff --git a/policy-controller/k8s/status/src/resource_id.rs b/policy-controller/k8s/status/src/resource_id.rs
index 4170673da..ab0397a4d 100644
--- a/policy-controller/k8s/status/src/resource_id.rs
+++ b/policy-controller/k8s/status/src/resource_id.rs
@@ -28,6 +28,7 @@ impl NamespaceGroupKindName {
         match (self.gkn.group.as_ref(), self.gkn.kind.as_ref()) {
             (POLICY_API_GROUP, "HTTPRoute") => Ok(linkerd_k8s_api::HttpRoute::api_version(&())),
             (GATEWAY_API_GROUP, "HTTPRoute") => Ok(k8s_gateway_api::HttpRoute::api_version(&())),
+            (GATEWAY_API_GROUP, "GRPCRoute") => Ok(k8s_gateway_api::GrpcRoute::api_version(&())),
             (group, kind) => {
                 anyhow::bail!("unknown group + kind combination: ({}, {})", group, kind)
             }
diff --git a/policy-controller/k8s/status/src/routes.rs b/policy-controller/k8s/status/src/routes.rs
index ffde765c3..6de3516d6 100644
--- a/policy-controller/k8s/status/src/routes.rs
+++ b/policy-controller/k8s/status/src/routes.rs
@@ -3,9 +3,10 @@ use linkerd_policy_controller_k8s_api::{
     self as k8s_core_api, gateway as k8s_gateway_api, policy as linkerd_k8s_api,
 };
 
+pub(crate) mod grpc;
 pub(crate) mod http;
 
-/// Represents a route's parent reference from its spec.
+/// Represents an xRoute's parent reference from its spec.
 ///
 /// This is separate from the policy controller index's `InboundParentRef`
 /// because it does not validate that the parent reference is not in another
diff --git a/policy-controller/k8s/status/src/routes/grpc.rs b/policy-controller/k8s/status/src/routes/grpc.rs
new file mode 100644
index 000000000..f43380963
--- /dev/null
+++ b/policy-controller/k8s/status/src/routes/grpc.rs
@@ -0,0 +1,173 @@
+use super::BackendReference;
+use linkerd_policy_controller_k8s_api::gateway as k8s_gateway_api;
+
+pub(crate) fn make_backends(
+    namespace: &str,
+    backends: impl Iterator<Item = k8s_gateway_api::GrpcRouteBackendRef>,
+) -> Vec<BackendReference> {
+    backends
+        .map(|backend_ref| BackendReference::from_backend_ref(&backend_ref.inner, namespace))
+        .collect()
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::index::POLICY_API_GROUP;
+    use linkerd_policy_controller_k8s_api::{self as k8s_core_api, gateway as k8s_gateway_api};
+
+    fn default_grpc_backends(
+        backend_refs: Vec<k8s_gateway_api::BackendObjectReference>,
+    ) -> Option<Vec<k8s_gateway_api::GrpcRouteBackendRef>> {
+        Some(
+            backend_refs
+                .into_iter()
+                .map(|backend_ref| k8s_gateway_api::GrpcRouteBackendRef {
+                    inner: backend_ref,
+                    weight: None,
+                    filters: None,
+                })
+                .collect(),
+        )
+    }
+
+    #[test]
+    fn backendrefs_from_route() {
+        let route = k8s_gateway_api::GrpcRoute {
+            metadata: k8s_core_api::ObjectMeta {
+                namespace: Some("foo".to_string()),
+                name: Some("foo".to_string()),
+                ..Default::default()
+            },
+            spec: k8s_gateway_api::GrpcRouteSpec {
+                inner: k8s_gateway_api::CommonRouteSpec { parent_refs: None },
+                hostnames: None,
+                rules: Some(vec![
+                    k8s_gateway_api::GrpcRouteRule {
+                        matches: None,
+                        filters: None,
+                        backend_refs: default_grpc_backends(vec![
+                            k8s_gateway_api::BackendObjectReference {
+                                group: None,
+                                kind: None,
+                                name: "ref-1".to_string(),
+                                namespace: Some("default".to_string()),
+                                port: None,
+                            },
+                            k8s_gateway_api::BackendObjectReference {
+                                group: None,
+                                kind: None,
+                                name: "ref-2".to_string(),
+                                namespace: None,
+                                port: None,
+                            },
+                        ]),
+                    },
+                    k8s_gateway_api::GrpcRouteRule {
+                        matches: None,
+                        filters: None,
+                        backend_refs: default_grpc_backends(vec![
+                            k8s_gateway_api::BackendObjectReference {
+                                group: Some("Core".to_string()),
+                                kind: Some("Service".to_string()),
+                                name: "ref-3".to_string(),
+                                namespace: Some("default".to_string()),
+                                port: None,
+                            },
+                        ]),
+                    },
+                    k8s_gateway_api::GrpcRouteRule {
+                        matches: None,
+                        filters: None,
+                        backend_refs: None,
+                    },
+                ]),
+            },
+            status: None,
+        };
+
+        let result = make_backends(
+            route
+                .metadata
+                .namespace
+                .as_deref()
+                .expect("GrpcRoute must have namespace"),
+            route
+                .spec
+                .rules
+                .into_iter()
+                .flatten()
+                .flat_map(|rule| rule.backend_refs)
+                .flatten(),
+        );
+        assert_eq!(
+            3,
+            result.len(),
+            "expected only three BackendReferences from route"
+        );
+        result.into_iter().for_each(|backend_ref| {
+            assert!(matches!(backend_ref, BackendReference::Service(_)));
+        })
+    }
+
+    #[test]
+    fn backendrefs_from_multiple_types() {
+        let route = k8s_gateway_api::GrpcRoute {
+            metadata: k8s_core_api::ObjectMeta {
+                namespace: Some("default".to_string()),
+                name: Some("foo".to_string()),
+                ..Default::default()
+            },
+            spec: k8s_gateway_api::GrpcRouteSpec {
+                inner: k8s_gateway_api::CommonRouteSpec { parent_refs: None },
+                hostnames: None,
+                rules: Some(vec![k8s_gateway_api::GrpcRouteRule {
+                    matches: None,
+                    filters: None,
+                    backend_refs: default_grpc_backends(vec![
+                        k8s_gateway_api::BackendObjectReference {
+                            group: None,
+                            kind: None,
+                            name: "ref-1".to_string(),
+                            namespace: None,
+                            port: None,
+                        },
+                        k8s_gateway_api::BackendObjectReference {
+                            group: Some(POLICY_API_GROUP.to_string()),
+                            kind: Some("Server".to_string()),
+                            name: "ref-2".to_string(),
+                            namespace: None,
+                            port: None,
+                        },
+                    ]),
+                }]),
+            },
+            status: None,
+        };
+
+        let result = make_backends(
+            route
+                .metadata
+                .namespace
+                .as_deref()
+                .expect("GrpcRoute must have namespace"),
+            route
+                .spec
+                .rules
+                .into_iter()
+                .flatten()
+                .flat_map(|rule| rule.backend_refs)
+                .flatten(),
+        );
+        assert_eq!(
+            2,
+            result.len(),
+            "expected only two BackendReferences from route"
+        );
+        let mut iter = result.into_iter();
+        let known = iter.next().unwrap();
+        assert!(matches!(known, BackendReference::Service(_)));
+        let unknown = iter.next().unwrap();
+        assert!(matches!(unknown, BackendReference::Unknown))
+    }
+}
diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs
index ec7e0da04..ac4d2b2a8 100644
--- a/policy-controller/src/admission.rs
+++ b/policy-controller/src/admission.rs
@@ -11,6 +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_index as index;
 use serde::de::DeserializeOwned;
 use std::task;
@@ -125,6 +126,10 @@ impl Admission {
             return self.admit_spec::<k8s_gateway_api::HttpRouteSpec>(req).await;
         }
 
+        if is_kind::<k8s_gateway_api::GrpcRoute>(&req) {
+            return self.admit_spec::<k8s_gateway_api::GrpcRouteSpec>(req).await;
+        }
+
         AdmissionResponse::invalid(format_args!(
             "unsupported resource type: {}.{}.{}",
             req.kind.group, req.kind.version, req.kind.kind
@@ -439,6 +444,8 @@ impl Validate<ServerAuthorizationSpec> for Admission {
     }
 }
 
+use index::routes::http as http_route;
+
 fn validate_match(
     httproute::HttpRouteMatch {
         path,
@@ -577,3 +584,73 @@ impl Validate<k8s_gateway_api::HttpRouteSpec> for Admission {
         Ok(())
     }
 }
+
+#[async_trait::async_trait]
+impl Validate<k8s_gateway_api::GrpcRouteSpec> for Admission {
+    async fn validate(
+        self,
+        _ns: &str,
+        _name: &str,
+        spec: k8s_gateway_api::GrpcRouteSpec,
+    ) -> Result<()> {
+        fn validate_filter(filter: k8s_gateway_api::GrpcRouteFilter) -> Result<()> {
+            match filter {
+                k8s_gateway_api::GrpcRouteFilter::RequestHeaderModifier {
+                    request_header_modifier,
+                } => http_route::header_modifier(request_header_modifier).map(|_| ()),
+                k8s_gateway_api::GrpcRouteFilter::ResponseHeaderModifier {
+                    response_header_modifier,
+                } => http_route::header_modifier(response_header_modifier).map(|_| ()),
+                k8s_gateway_api::GrpcRouteFilter::RequestMirror { .. } => Ok(()),
+                k8s_gateway_api::GrpcRouteFilter::ExtensionRef { .. } => Ok(()),
+            }
+        }
+
+        fn validate_match_rule(
+            k8s_gateway_api::GrpcRouteMatch { method, headers }: k8s_gateway_api::GrpcRouteMatch,
+        ) -> Result<()> {
+            if let Some(method_match) = method {
+                let (method_name, service_name) = match method_match {
+                    k8s_gateway_api::GrpcMethodMatch::Exact { method, service } => {
+                        (method, service)
+                    }
+                    k8s_gateway_api::GrpcMethodMatch::RegularExpression { method, service } => {
+                        (method, service)
+                    }
+                };
+
+                if method_name.as_deref().map(str::is_empty).unwrap_or(true)
+                    && service_name.as_deref().map(str::is_empty).unwrap_or(true)
+                {
+                    bail!("at least one of GrpcMethodMatch.Service and GrpcMethodMatch.Method MUST be a non-empty string")
+                }
+            }
+
+            for rule in headers.into_iter().flatten() {
+                http_route::header_match(rule)?;
+            }
+
+            Ok(())
+        }
+
+        // Validate the rules in this spec.
+        // This is essentially just a check to ensure that none
+        // of the rules are improperly constructed (e.g. include
+        // a `GrpcMethodMatch` rule where neither `method.method`
+        // nor `method.service` actually contain a value)
+        for k8s_gateway_api::GrpcRouteRule {
+            filters, matches, ..
+        } in spec.rules.into_iter().flatten()
+        {
+            for rule in matches.into_iter().flatten() {
+                validate_match_rule(rule)?;
+            }
+
+            for filter in filters.into_iter().flatten() {
+                validate_filter(filter)?;
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/policy-controller/src/main.rs b/policy-controller/src/main.rs
index 126baf0e4..552509921 100644
--- a/policy-controller/src/main.rs
+++ b/policy-controller/src/main.rs
@@ -12,6 +12,7 @@ use linkerd_policy_controller::{
     grpc, inbound, index_list::IndexList, k8s, outbound, Admission, ClusterInfo, DefaultPolicy,
     InboundDiscover, IpNet, OutboundDiscover,
 };
+use linkerd_policy_controller_k8s_api::gateway as k8s_gateway_api;
 use linkerd_policy_controller_k8s_index::ports::parse_portset;
 use linkerd_policy_controller_k8s_status::{self as status};
 use prometheus_client::registry::Registry;
@@ -259,6 +260,13 @@ async fn main() -> Result<()> {
             .instrument(info_span!("httproutes.gateway.networking.k8s.io")),
     );
 
+    let gateway_grpc_routes =
+        runtime.watch_all::<k8s_gateway_api::GrpcRoute>(watcher::Config::default());
+    tokio::spawn(
+        kubert::index::namespaced(status_index.clone(), gateway_grpc_routes)
+            .instrument(info_span!("grpcroutes.gateway.networking.k8s.io")),
+    );
+
     let services = runtime.watch_all::<k8s::Service>(watcher::Config::default());
     let services_indexes = IndexList::new(outbound_index.clone())
         .push(status_index.clone())


Test patch:
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index 8a96e7c66..a948dfcd1 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1310,7 +1313,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 579a2f931900ccee9dc86afedf01af0e4ca273ef6ec649b2630c358daa1e067f
+        checksum/config: 7ed64a2dea184e1e880a33bb5577b3f6c2a3fdf72492321683bbdaef30a1f365
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index e19b5b2c8..4fecab99e 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1309,7 +1312,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 579a2f931900ccee9dc86afedf01af0e4ca273ef6ec649b2630c358daa1e067f
+        checksum/config: 7ed64a2dea184e1e880a33bb5577b3f6c2a3fdf72492321683bbdaef30a1f365
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index a4d301f37..8ff609cc0 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1309,7 +1312,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 579a2f931900ccee9dc86afedf01af0e4ca273ef6ec649b2630c358daa1e067f
+        checksum/config: 7ed64a2dea184e1e880a33bb5577b3f6c2a3fdf72492321683bbdaef30a1f365
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index e19b5b2c8..4fecab99e 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1309,7 +1312,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 579a2f931900ccee9dc86afedf01af0e4ca273ef6ec649b2630c358daa1e067f
+        checksum/config: 7ed64a2dea184e1e880a33bb5577b3f6c2a3fdf72492321683bbdaef30a1f365
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_default_override_dst_get_nets.golden b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
index 26db4531b..879966f3b 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1309,7 +1312,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 579a2f931900ccee9dc86afedf01af0e4ca273ef6ec649b2630c358daa1e067f
+        checksum/config: 7ed64a2dea184e1e880a33bb5577b3f6c2a3fdf72492321683bbdaef30a1f365
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_default_token.golden b/cli/cmd/testdata/install_default_token.golden
index a427cbc76..faacabc09 100644
--- a/cli/cmd/testdata/install_default_token.golden
+++ b/cli/cmd/testdata/install_default_token.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1300,7 +1303,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 579a2f931900ccee9dc86afedf01af0e4ca273ef6ec649b2630c358daa1e067f
+        checksum/config: 7ed64a2dea184e1e880a33bb5577b3f6c2a3fdf72492321683bbdaef30a1f365
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_gid_output.golden b/cli/cmd/testdata/install_gid_output.golden
index 490a2e9cb..a379191a9 100755
--- a/cli/cmd/testdata/install_gid_output.golden
+++ b/cli/cmd/testdata/install_gid_output.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1313,7 +1316,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 579a2f931900ccee9dc86afedf01af0e4ca273ef6ec649b2630c358daa1e067f
+        checksum/config: 7ed64a2dea184e1e880a33bb5577b3f6c2a3fdf72492321683bbdaef30a1f365
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 89a605005..6b7b6b6a4 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1407,7 +1410,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 8f967c9ecfe4fb09646dfa503d3831a391288c65fb8b77200d2b3c9390620c1e
+        checksum/config: 92d3d7185abd241437f7889dabe15a840b3b91f8910f9491ab10de3a1074ed6d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index cf49ddda8..47419154f 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1407,7 +1410,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 8f967c9ecfe4fb09646dfa503d3831a391288c65fb8b77200d2b3c9390620c1e
+        checksum/config: 92d3d7185abd241437f7889dabe15a840b3b91f8910f9491ab10de3a1074ed6d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 47dfa71d8..505e51e6e 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1240,7 +1243,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 579a2f931900ccee9dc86afedf01af0e4ca273ef6ec649b2630c358daa1e067f
+        checksum/config: 7ed64a2dea184e1e880a33bb5577b3f6c2a3fdf72492321683bbdaef30a1f365
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_helm_control_plane_output.golden b/cli/cmd/testdata/install_helm_control_plane_output.golden
index f61bdb452..bb04189aa 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output.golden
@@ -177,6 +177,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -219,6 +220,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -233,6 +235,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1284,7 +1287,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: bdf627c4f8fc3bc85e69064de22d6032ab30887fa55d18238a677e9b9b594dd8
+        checksum/config: aae2f97af199e93464b02da923a0732b603488c822241c14b2d86ba2bf6da03d
         linkerd.io/created-by: linkerd/helm linkerd-version
         linkerd.io/proxy-version: test-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
index d815380a7..38d4b2cac 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
@@ -177,6 +177,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -219,6 +220,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -233,6 +235,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1382,7 +1385,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: b0c26a237398c80aaed48f6954a403fc169549f3b7e927bdd86c71f8d13c8762
+        checksum/config: 2b4f4db151be1b48cf7ccff4b16df4d7eb45dee57b998d50cfe13eec8efbaac7
         linkerd.io/created-by: linkerd/helm linkerd-version
         linkerd.io/proxy-version: test-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden b/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden
index d04c026d9..ac92e9443 100755
--- a/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden
@@ -177,6 +177,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -219,6 +220,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -233,6 +235,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1386,7 +1389,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: b0c26a237398c80aaed48f6954a403fc169549f3b7e927bdd86c71f8d13c8762
+        checksum/config: 2b4f4db151be1b48cf7ccff4b16df4d7eb45dee57b998d50cfe13eec8efbaac7
         linkerd.io/created-by: linkerd/helm linkerd-version
         linkerd.io/proxy-version: test-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden
index 53095c182..d3b1d38a7 100644
--- a/cli/cmd/testdata/install_helm_output_ha_labels.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden
@@ -177,6 +177,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -219,6 +220,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -233,6 +235,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1390,7 +1393,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: b0c26a237398c80aaed48f6954a403fc169549f3b7e927bdd86c71f8d13c8762
+        checksum/config: 2b4f4db151be1b48cf7ccff4b16df4d7eb45dee57b998d50cfe13eec8efbaac7
         linkerd.io/created-by: linkerd/helm linkerd-version
         linkerd.io/proxy-version: test-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
index 614bbbe5d..535b7ba64 100644
--- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
@@ -177,6 +177,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -219,6 +220,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -233,6 +235,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1372,7 +1375,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 467bcfd10f346970575701d5199fde803e9f61e9039e05d4c765d3c5524804b4
+        checksum/config: 92e9069580ccc3e2e6a5a23fe9d4586ae8f0f3b48b906119e6f884ff66ea7395
         linkerd.io/created-by: linkerd/helm linkerd-version
         linkerd.io/proxy-version: test-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 4d0ff330d..3159a5348 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1302,7 +1305,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 579a2f931900ccee9dc86afedf01af0e4ca273ef6ec649b2630c358daa1e067f
+        checksum/config: 7ed64a2dea184e1e880a33bb5577b3f6c2a3fdf72492321683bbdaef30a1f365
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index 5b8d54813..133dc1bc9 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -183,6 +183,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -225,6 +226,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -239,6 +241,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1250,7 +1253,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 91d0273ac7d213bf95872f62c460ba146a459106e21d12f75e2ebe6ad7562b7f
+        checksum/config: 7523675f365af90bf6840b8b1eb0b12bbb4c819738cc9353608cbd16a9bf4196
         linkerd.io/created-by: CliVersion
         linkerd.io/proxy-version: ProxyVersion
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index 98514edba..2973f1b0c 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1309,7 +1312,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 579a2f931900ccee9dc86afedf01af0e4ca273ef6ec649b2630c358daa1e067f
+        checksum/config: 7ed64a2dea184e1e880a33bb5577b3f6c2a3fdf72492321683bbdaef30a1f365
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index 500925872..60b479156 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -186,6 +186,7 @@ webhooks:
     apiVersions: ["*"]
     resources:
     - httproutes
+    - grpcroutes
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -228,6 +229,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes
+      - grpcroutes
     verbs:
       - get
       - list
@@ -242,6 +244,7 @@ rules:
       - gateway.networking.k8s.io
     resources:
       - httproutes/status
+      - grpcroutes/status
     verbs:
       - patch
   - apiGroups:
@@ -1309,7 +1312,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 579a2f931900ccee9dc86afedf01af0e4ca273ef6ec649b2630c358daa1e067f
+        checksum/config: 7ed64a2dea184e1e880a33bb5577b3f6c2a3fdf72492321683bbdaef30a1f365
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/policy-controller/k8s/status/src/tests/routes.rs b/policy-controller/k8s/status/src/tests/routes.rs
index 1d9848bfb..a0d7af02b 100644
--- a/policy-controller/k8s/status/src/tests/routes.rs
+++ b/policy-controller/k8s/status/src/tests/routes.rs
@@ -1,5 +1,6 @@
 use linkerd_policy_controller_k8s_api::{self as k8s_core_api, policy as linkerd_k8s_api};
 
+mod grpc;
 mod http;
 
 fn make_service(
diff --git a/policy-controller/k8s/status/src/tests/routes/grpc.rs b/policy-controller/k8s/status/src/tests/routes/grpc.rs
new file mode 100644
index 000000000..006e1862d
--- /dev/null
+++ b/policy-controller/k8s/status/src/tests/routes/grpc.rs
@@ -0,0 +1,528 @@
+use crate::{index::POLICY_API_GROUP, resource_id::NamespaceGroupKindName, Index, IndexMetrics};
+use chrono::{DateTime, Utc};
+use kubert::index::IndexNamespacedResource;
+use linkerd_policy_controller_core::{routes::GroupKindName, POLICY_CONTROLLER_NAME};
+use linkerd_policy_controller_k8s_api::{
+    self as k8s_core_api, gateway as k8s_gateway_api, policy as linkerd_k8s_api, Resource,
+    ResourceExt,
+};
+use std::sync::Arc;
+use tokio::sync::{mpsc, watch};
+
+#[test]
+fn route_with_no_backends() {
+    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());
+
+    // Apply the route.
+    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),
+    };
+    let 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: "route-foo".into(),
+        },
+    };
+    let route = make_route(&id, parent.clone(), None);
+    index.write().apply(route);
+
+    // Create the expected update.
+    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,
+        controller_name: POLICY_CONTROLLER_NAME.to_string(),
+        conditions: vec![accepted_condition, backend_condition],
+    };
+    let status = make_status(vec![parent_status]);
+    let patch = crate::index::make_patch(&id, status).unwrap();
+
+    let update = updates_rx.try_recv().unwrap();
+    assert_eq!(id, update.id);
+    assert_eq!(patch, update.patch);
+    assert!(updates_rx.try_recv().is_err())
+}
+
+#[test]
+fn route_with_valid_backends() {
+    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());
+
+    // Apply one backend service
+    let backend1 = super::make_service("ns-0", "backend-1");
+    index.write().apply(backend1.clone());
+
+    // Apply one backend service
+    let backend2 = super::make_service("ns-0", "backend-2");
+    index.write().apply(backend2.clone());
+
+    // Apply the route.
+    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),
+    };
+    let 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: "route-foo".into(),
+        },
+    };
+    let route = make_route(
+        &id,
+        parent.clone(),
+        Some(vec![
+            k8s_gateway_api::GrpcRouteBackendRef {
+                inner: k8s_gateway_api::BackendObjectReference {
+                    group: Some("core".to_string()),
+                    kind: Some("Service".to_string()),
+                    name: backend1.name_unchecked(),
+                    namespace: backend1.namespace(),
+                    port: Some(8080),
+                },
+                filters: None,
+                weight: None,
+            },
+            k8s_gateway_api::GrpcRouteBackendRef {
+                inner: k8s_gateway_api::BackendObjectReference {
+                    group: Some("core".to_string()),
+                    kind: Some("Service".to_string()),
+                    name: backend2.name_unchecked(),
+                    namespace: backend2.namespace(),
+                    port: Some(8080),
+                },
+                filters: None,
+                weight: None,
+            },
+        ]),
+    );
+    index.write().apply(route);
+
+    // Create the expected update.
+    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(),
+    };
+    // All backends exist and can be resolved.
+    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,
+        controller_name: POLICY_CONTROLLER_NAME.to_string(),
+        conditions: vec![accepted_condition, backend_condition],
+    };
+    let status = make_status(vec![parent_status]);
+    let patch = crate::index::make_patch(&id, status).unwrap();
+
+    let update = updates_rx.try_recv().unwrap();
+    assert_eq!(id, update.id);
+    assert_eq!(patch, update.patch);
+    assert!(updates_rx.try_recv().is_err())
+}
+
+#[test]
+fn route_with_invalid_backend() {
+    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());
+
+    // Apply one backend service
+    let backend = super::make_service("ns-0", "backend-1");
+    index.write().apply(backend.clone());
+
+    // Apply the route.
+    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),
+    };
+    let 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: "route-foo".into(),
+        },
+    };
+    let route = make_route(
+        &id,
+        parent.clone(),
+        Some(vec![
+            k8s_gateway_api::GrpcRouteBackendRef {
+                inner: k8s_gateway_api::BackendObjectReference {
+                    group: Some("core".to_string()),
+                    kind: Some("Service".to_string()),
+                    name: backend.name_unchecked(),
+                    namespace: backend.namespace(),
+                    port: Some(8080),
+                },
+                filters: None,
+                weight: None,
+            },
+            k8s_gateway_api::GrpcRouteBackendRef {
+                inner: k8s_gateway_api::BackendObjectReference {
+                    group: Some("core".to_string()),
+                    kind: Some("Service".to_string()),
+                    name: "nonexistant-backend".to_string(),
+                    namespace: backend.namespace(),
+                    port: Some(8080),
+                },
+                filters: None,
+                weight: None,
+            },
+        ]),
+    );
+    index.write().apply(route);
+
+    // Create the expected update.
+    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(),
+    };
+    // One of the backends does not exist so the status should be BackendNotFound.
+    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: "BackendNotFound".to_string(),
+        status: "False".to_string(),
+        type_: "ResolvedRefs".to_string(),
+    };
+    let parent_status = k8s_gateway_api::RouteParentStatus {
+        parent_ref: parent,
+        controller_name: POLICY_CONTROLLER_NAME.to_string(),
+        conditions: vec![accepted_condition, backend_condition],
+    };
+    let status = make_status(vec![parent_status]);
+    let patch = crate::index::make_patch(&id, status).unwrap();
+
+    let update = updates_rx.try_recv().unwrap();
+    assert_eq!(id, update.id);
+    assert_eq!(patch, update.patch);
+    assert!(updates_rx.try_recv().is_err())
+}
+
+#[test]
+fn route_accepted_after_server_create() {
+    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()),
+    );
+
+    // Create the route id and route
+    let id = NamespaceGroupKindName {
+        namespace: "ns-0".to_string(),
+        gkn: GroupKindName {
+            name: "route-foo".into(),
+            kind: k8s_gateway_api::GrpcRoute::kind(&()),
+            group: k8s_gateway_api::GrpcRoute::group(&()),
+        },
+    };
+    let parent = k8s_gateway_api::ParentReference {
+        group: Some(POLICY_API_GROUP.to_string()),
+        kind: Some("Server".to_string()),
+        namespace: None,
+        name: "srv-8080".to_string(),
+        section_name: None,
+        port: None,
+    };
+    let route = make_route(&id, parent, None);
+
+    // Apply the route.
+    index.write().apply(route);
+
+    // Create the expected update.
+    let parent_status = make_parent_status(
+        &id.namespace,
+        "srv-8080",
+        "Accepted",
+        "False",
+        "NoMatchingParent",
+    );
+    let status = make_status(vec![parent_status]);
+    let patch = crate::index::make_patch(&id, status).unwrap();
+
+    // The first update will be that the GRPCRoute is not accepted because the
+    // Server has been created yet.
+    let update = updates_rx.try_recv().unwrap();
+    assert_eq!(id, update.id);
+    assert_eq!(patch, update.patch);
+
+    // Apply the server
+    let server = super::make_server(
+        "ns-0",
+        "srv-8080",
+        8080,
+        Some(("app", "app-0")),
+        Some(("app", "app-0")),
+        Some(linkerd_k8s_api::server::ProxyProtocol::Http2),
+    );
+    index.write().apply(server);
+
+    // Create the expected update.
+    let parent_status =
+        make_parent_status(&id.namespace, "srv-8080", "Accepted", "True", "Accepted");
+    let status = make_status(vec![parent_status]);
+    let patch = crate::index::make_patch(&id, status).unwrap();
+
+    // The second update will be that the GRPCRoute is accepted because the
+    // Server has been created.
+    let update = updates_rx.try_recv().unwrap();
+    assert_eq!(id, update.id);
+    assert_eq!(patch, update.patch);
+    assert!(updates_rx.try_recv().is_err())
+}
+
+#[test]
+fn route_rejected_after_server_delete() {
+    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()),
+    );
+
+    let server = super::make_server(
+        "ns-0",
+        "srv-8080",
+        8080,
+        Some(("app", "app-0")),
+        Some(("app", "app-0")),
+        Some(linkerd_k8s_api::server::ProxyProtocol::Http2),
+    );
+    index.write().apply(server);
+
+    // There should be no update since there are no GRPCRoutes yet.
+    assert!(updates_rx.try_recv().is_err());
+
+    // Create the route id and route
+    let id = NamespaceGroupKindName {
+        namespace: "ns-0".to_string(),
+        gkn: GroupKindName {
+            name: "route-foo".into(),
+            kind: k8s_gateway_api::GrpcRoute::kind(&()),
+            group: k8s_gateway_api::GrpcRoute::group(&()),
+        },
+    };
+    let parent = k8s_gateway_api::ParentReference {
+        group: Some(POLICY_API_GROUP.to_string()),
+        kind: Some("Server".to_string()),
+        namespace: None,
+        name: "srv-8080".to_string(),
+        section_name: None,
+        port: None,
+    };
+    let route = make_route(&id, parent, None);
+
+    // Apply the route
+    index.write().apply(route);
+
+    // Create the expected update.
+    let parent_status =
+        make_parent_status(&id.namespace, "srv-8080", "Accepted", "True", "Accepted");
+    let status = make_status(vec![parent_status]);
+    let patch = crate::index::make_patch(&id, status).unwrap();
+
+    // The second update will be that the GRPCRoute is accepted because the
+    // Server has been created.
+    let update = updates_rx.try_recv().unwrap();
+    assert_eq!(id, update.id);
+    assert_eq!(patch, update.patch);
+
+    {
+        let mut index = index.write();
+        <Index as IndexNamespacedResource<linkerd_k8s_api::Server>>::delete(
+            &mut index,
+            "ns-0".to_string(),
+            "srv-8080".to_string(),
+        );
+    }
+
+    // Create the expected update.
+    let parent_status =
+        make_parent_status("ns-0", "srv-8080", "Accepted", "False", "NoMatchingParent");
+    let status = make_status(vec![parent_status]);
+    let patch = crate::index::make_patch(&id, status).unwrap();
+
+    // The third update will be that the GRPCRoute is not accepted because the
+    // Server has been deleted.
+    let update = updates_rx.try_recv().unwrap();
+    assert_eq!(id, update.id);
+    assert_eq!(patch, update.patch);
+    assert!(updates_rx.try_recv().is_err());
+}
+
+fn make_route(
+    id: &NamespaceGroupKindName,
+    parent: k8s_gateway_api::ParentReference,
+    backends: Option<Vec<k8s_gateway_api::GrpcRouteBackendRef>>,
+) -> k8s_gateway_api::GrpcRoute {
+    k8s_gateway_api::GrpcRoute {
+        status: None,
+        metadata: k8s_core_api::ObjectMeta {
+            name: Some(id.gkn.name.to_string()),
+            namespace: Some(id.namespace.clone()),
+            creation_timestamp: Some(k8s_core_api::Time(Utc::now())),
+            ..Default::default()
+        },
+        spec: k8s_gateway_api::GrpcRouteSpec {
+            inner: k8s_gateway_api::CommonRouteSpec {
+                parent_refs: Some(vec![parent]),
+            },
+            hostnames: None,
+            rules: Some(vec![k8s_gateway_api::GrpcRouteRule {
+                filters: None,
+                backend_refs: backends,
+                matches: Some(vec![k8s_gateway_api::GrpcRouteMatch {
+                    headers: None,
+                    method: Some(k8s_gateway_api::GrpcMethodMatch::Exact {
+                        method: Some("MakeRoute".to_string()),
+                        service: Some("io.linkerd.Test".to_string()),
+                    }),
+                }]),
+            }]),
+        },
+    }
+}
+
+fn make_status(
+    parents: Vec<k8s_gateway_api::RouteParentStatus>,
+) -> k8s_gateway_api::GrpcRouteStatus {
+    k8s_gateway_api::GrpcRouteStatus {
+        inner: k8s_gateway_api::RouteStatus { parents },
+    }
+}
+
+fn make_parent_status(
+    namespace: impl ToString,
+    name: impl ToString,
+    type_: impl ToString,
+    status: impl ToString,
+    reason: impl ToString,
+) -> k8s_gateway_api::RouteParentStatus {
+    let condition = k8s_core_api::Condition {
+        message: "".to_string(),
+        type_: type_.to_string(),
+        observed_generation: None,
+        reason: reason.to_string(),
+        status: status.to_string(),
+        last_transition_time: k8s_core_api::Time(DateTime::<Utc>::MIN_UTC),
+    };
+    k8s_gateway_api::RouteParentStatus {
+        conditions: vec![condition],
+        parent_ref: k8s_gateway_api::ParentReference {
+            port: None,
+            section_name: None,
+            name: name.to_string(),
+            kind: Some("Server".to_string()),
+            namespace: Some(namespace.to_string()),
+            group: Some(POLICY_API_GROUP.to_string()),
+        },
+        controller_name: POLICY_CONTROLLER_NAME.to_string(),
+    }
+}
diff --git a/policy-test/Cargo.toml b/policy-test/Cargo.toml
index 4f2f3ced4..2625638cb 100644
--- a/policy-test/Cargo.toml
+++ b/policy-test/Cargo.toml
@@ -10,7 +10,7 @@ anyhow = "1"
 hyper = { version = "0.14", features = ["client", "http2"] }
 futures = { version = "0.3", default-features = false }
 ipnet = "2"
-k8s-gateway-api = "0.15"
+k8s-gateway-api = "0.16"
 k8s-openapi = { version = "0.20", features = ["v1_22"] }
 linkerd-policy-controller-core = { path = "../policy-controller/core" }
 linkerd-policy-controller-k8s-api = { path = "../policy-controller/k8s/api" }