Repository: envoyproxy/ai-gateway
Pull request #187 (2025-01-27 09:40:17)

controller: update AIGatewayRoute status when reconcile
**Commit Message**

Update AIGatewayRoute status during reconcile.

**Related Issues/PRs (if applicable)**

Contributes to #130


netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *envoy-ai-gateway* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | e537f3d7193647dfb2db464dff864387ecc64a36 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/envoy-ai-gateway/deploys/679754844fb08e0008c025c0 |
mathetake: so the gateway route reconciliation result is not only there - it's actually happening in sink.go. so the current change won't be enough 
daixiang0: @mathetake please review.
mathetake: @daixiang0 please read CONTRIBUTING.md carefully and do not force push in this project (the same rule as to envoyproxy/envoy).
daixiang0: @mathetake I know, thanks, I will update.
mathetake: @daixiang0 thanks for your hard work here- could you fix the coverage test as well as the end to end test (the log seems to indicate that controller is crashing? idk)
daixiang0: @mathetake sure, I will do it.
daixiang0: @mathetake hi, now we only have happy case, the most codes I added are for bad case, do you mind I add coverage-ignore for failure logic?
daixiang0: @mathetake I find one problem:

We use `DeepCopy()` here, so update status in the `sink.go` seems useless :(
https://github.com/daixiang0/ai-gateway/blob/537ac62f4c20528fa567a5d6522b01decf577f99/internal/controller/ai_gateway_route.go#L117


mathetake: why?
daixiang0: Since the aiGatewayRoute sent to sink is a copy, any changes to it does not show in the original one, which means when use `kubectl describe` to check the original one's status, the changes happen in the sink do not show.
mathetake: hmm i am not sure but can we just use Patch append status?
daixiang0: Why need `DeepCopy()` here?
daixiang0: A strange issue:

```
time=2025-02-08T05:32:49.745Z level=ERROR msg="failed to patch AIGatewayRoute status" err="aigatewayroutes.aigateway.envoyproxy.io \"route1\" not found" namespace=ns1 name=route1
```

I use `Get()` to get the original resource and patch it, but `Patch()` said not found, need time to debug.

@mathetake @leseb any clue may help me!
daixiang0: There are some bad cases that I do not find a good way to test, add ignore for now.
daixiang0: @mathetake please review.
mathetake: ok finally getting around to this
daixiang0: Let me try.
daixiang0: @mathetake updated, please review again, thank you.
mathetake: @daixiang0 thank you for the hard work! We are working towards the initial release and before cutting rc1, I will do some refactoring the controller package which affects this PR. will get back to this after that!
mathetake: so in #364 I removed the sink stuff, so this will be more simpler but needs a rework (sorry). Thank you for your patience @daixiang0 
daixiang0: @mathetake do we need to rebase not merge the main branch for now:

![image](https://github.com/user-attachments/assets/d1d531cb-6711-4cea-a729-f66bca421009)

daixiang0: After review completed, I will open a new PR to fix DCO and merge issue, or I need force push.
mathetake: you can force push for fixing DCO (that's the only exception) :D 
daixiang0: Too much error when rebase, sorry I give up :(
mathetake: can you modify tests/controller/*_test.go to test the behavior with envtest?
mathetake: i will take it from here!

Patch:
diff --git a/Makefile b/Makefile
index 9f8275f..5b7d770 100644
--- a/Makefile
+++ b/Makefile
@@ -144,8 +144,7 @@ ENVTEST_K8S_VERSIONS ?= 1.29.0 1.30.0 1.31.0
 test-crdcel: apigen
 	@for k8sVersion in $(ENVTEST_K8S_VERSIONS); do \
   		echo "Run CEL Validation on k8s $$k8sVersion"; \
-        KUBEBUILDER_ASSETS="$$(go tool setup-envtest use $$k8sVersion -p path)" \
-                 go test ./tests/crdcel $(GO_TEST_ARGS) $(GO_TEST_E2E_ARGS) --tags test_crdcel; \
+        ENVTEST_K8S_VERSION=$$k8sVersion go test ./tests/crdcel $(GO_TEST_ARGS) $(GO_TEST_E2E_ARGS) --tags test_crdcel; \
     done
 
 # This runs the end-to-end tests for extproc without controller or k8s at all.
@@ -164,8 +163,7 @@ test-extproc: build.extproc
 test-controller: apigen
 	@for k8sVersion in $(ENVTEST_K8S_VERSIONS); do \
   		echo "Run Controller tests on k8s $$k8sVersion"; \
-        KUBEBUILDER_ASSETS="$$(go tool setup-envtest use $$k8sVersion -p path)" \
-                 go test ./tests/controller $(GO_TEST_ARGS) $(GO_TEST_E2E_ARGS) -tags test_controller; \
+        ENVTEST_K8S_VERSION=$$k8sVersion go test ./tests/controller $(GO_TEST_ARGS) $(GO_TEST_E2E_ARGS) -tags test_controller; \
     done
 
 # This runs the end-to-end tests for the controller and extproc with a local kind cluster.
diff --git a/api/v1alpha1/api.go b/api/v1alpha1/api.go
index 1b29c47..61af661 100644
--- a/api/v1alpha1/api.go
+++ b/api/v1alpha1/api.go
@@ -14,6 +14,7 @@ import (
 )
 
 // +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
 
 // AIGatewayRoute combines multiple AIServiceBackends and attaching them to Gateway(s) resources.
 //
@@ -45,6 +46,14 @@ type AIGatewayRoute struct {
 	metav1.ObjectMeta `json:"metadata,omitempty"`
 	// Spec defines the details of the AIGatewayRoute.
 	Spec AIGatewayRouteSpec `json:"spec,omitempty"`
+	// Status defines the status details of the AIGatewayRoute.
+	Status AIGatewayRouteStatus `json:"status,omitempty"`
+}
+
+// AIGatewayRouteStatus contains the conditions by the reconciliation result.
+type AIGatewayRouteStatus struct {
+	// Conditions is the list of conditions by the reconciliation result.
+	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index dd01ffa..5d20b91 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -11,8 +11,9 @@ package v1alpha1
 
 import (
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
-	"sigs.k8s.io/gateway-api/apis/v1"
+	apisv1 "sigs.k8s.io/gateway-api/apis/v1"
 	"sigs.k8s.io/gateway-api/apis/v1alpha2"
 )
 
@@ -67,6 +68,7 @@ func (in *AIGatewayRoute) DeepCopyInto(out *AIGatewayRoute) {
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
 	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AIGatewayRoute.
@@ -166,7 +168,7 @@ func (in *AIGatewayRouteRuleMatch) DeepCopyInto(out *AIGatewayRouteRuleMatch) {
 	*out = *in
 	if in.Headers != nil {
 		in, out := &in.Headers, &out.Headers
-		*out = make([]v1.HTTPHeaderMatch, len(*in))
+		*out = make([]apisv1.HTTPHeaderMatch, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -225,6 +227,28 @@ func (in *AIGatewayRouteSpec) DeepCopy() *AIGatewayRouteSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AIGatewayRouteStatus) DeepCopyInto(out *AIGatewayRouteStatus) {
+	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AIGatewayRouteStatus.
+func (in *AIGatewayRouteStatus) DeepCopy() *AIGatewayRouteStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(AIGatewayRouteStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AIServiceBackend) DeepCopyInto(out *AIServiceBackend) {
 	*out = *in
@@ -290,12 +314,12 @@ func (in *AIServiceBackendSpec) DeepCopyInto(out *AIServiceBackendSpec) {
 	in.BackendRef.DeepCopyInto(&out.BackendRef)
 	if in.BackendSecurityPolicyRef != nil {
 		in, out := &in.BackendSecurityPolicyRef, &out.BackendSecurityPolicyRef
-		*out = new(v1.LocalObjectReference)
+		*out = new(apisv1.LocalObjectReference)
 		**out = **in
 	}
 	if in.Timeouts != nil {
 		in, out := &in.Timeouts, &out.Timeouts
-		*out = new(v1.HTTPRouteTimeouts)
+		*out = new(apisv1.HTTPRouteTimeouts)
 		(*in).DeepCopyInto(*out)
 	}
 }
@@ -315,7 +339,7 @@ func (in *AWSCredentialsFile) DeepCopyInto(out *AWSCredentialsFile) {
 	*out = *in
 	if in.SecretRef != nil {
 		in, out := &in.SecretRef, &out.SecretRef
-		*out = new(v1.SecretObjectReference)
+		*out = new(apisv1.SecretObjectReference)
 		(*in).DeepCopyInto(*out)
 	}
 }
@@ -377,7 +401,7 @@ func (in *BackendSecurityPolicyAPIKey) DeepCopyInto(out *BackendSecurityPolicyAP
 	*out = *in
 	if in.SecretRef != nil {
 		in, out := &in.SecretRef, &out.SecretRef
-		*out = new(v1.SecretObjectReference)
+		*out = new(apisv1.SecretObjectReference)
 		(*in).DeepCopyInto(*out)
 	}
 }
diff --git a/internal/controller/ai_gateway_route.go b/internal/controller/ai_gateway_route.go
index af5aba4..9fb496c 100644
--- a/internal/controller/ai_gateway_route.go
+++ b/internal/controller/ai_gateway_route.go
@@ -7,8 +7,10 @@ package controller
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"path"
+	"sort"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/go-logr/logr"
@@ -94,14 +96,27 @@ func (c *AIGatewayRouteController) Reconcile(ctx context.Context, req reconcile.
 	// TODO: merge this into syncAIGatewayRoute. This is a left over from the previous sink based implementation.
 	c.logger.Info("Ensuring extproc configmap exists", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name)
 	if err := c.ensuresExtProcConfigMapExists(ctx, &aiGatewayRoute); err != nil {
-		return ctrl.Result{}, fmt.Errorf("failed to ensure extproc configmap exists: %w", err)
+		c.logger.Error(err, "failed to reconcile extProc config map")
+		message := fmt.Sprintf("failed to ensure extproc configmap exists: %v", err)
+		c.updateAIGatewayRouteStatus(ctx, &aiGatewayRoute, false, message)
+		return ctrl.Result{}, errors.New(message)
 	}
 	// TODO: merge this into syncAIGatewayRoute. This is a left over from the previous sink based implementation.
 	c.logger.Info("Reconciling extension policy", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name)
 	if err := c.reconcileExtProcExtensionPolicy(ctx, &aiGatewayRoute); err != nil {
-		return ctrl.Result{}, fmt.Errorf("failed to reconcile extension policy: %w", err)
+		c.logger.Error(err, "failed to reconcile extension policy")
+		message := fmt.Sprintf("failed to reconcile extension policy: %v", err)
+		c.updateAIGatewayRouteStatus(ctx, &aiGatewayRoute, false, message)
+		return ctrl.Result{}, errors.New(message)
 	}
-	return reconcile.Result{}, c.syncAIGatewayRoute(ctx, &aiGatewayRoute)
+
+	if err := c.syncAIGatewayRoute(ctx, &aiGatewayRoute); err != nil {
+		c.logger.Error(err, "failed to sync AIGatewayRoute")
+		c.updateAIGatewayRouteStatus(ctx, &aiGatewayRoute, false, err.Error())
+		return ctrl.Result{}, err
+	}
+	c.updateAIGatewayRouteStatus(ctx, &aiGatewayRoute, true, "AI Gateway Route reconciled successfully")
+	return reconcile.Result{}, nil
 }
 
 // reconcileExtProcExtensionPolicy creates or updates the extension policy for the external process.
@@ -678,3 +693,45 @@ func backendSecurityPolicyVolumeName(ruleIndex, backendRefIndex int, name string
 func backendSecurityMountPath(backendSecurityPolicyKey string) string {
 	return fmt.Sprintf("%s/%s", mountedExtProcSecretPath, backendSecurityPolicyKey)
 }
+
+const (
+	aiGatewayRouteConditionTypeAccepted    = "Accepted"
+	aiGatewayRouteConditionTypeNotAccepted = "NotAccepted"
+)
+
+// updateAIGatewayRouteStatus updates the status of the AIGatewayRoute.
+func (c *AIGatewayRouteController) updateAIGatewayRouteStatus(ctx context.Context, route *aigv1a1.AIGatewayRoute, accepted bool, message string) {
+	route = route.DeepCopy()
+	condition := metav1.Condition{Message: message, LastTransitionTime: metav1.Now()}
+	if accepted {
+		condition.Type = aiGatewayRouteConditionTypeAccepted
+		condition.Reason = "ReconciliationSucceeded"
+		condition.Status = metav1.ConditionTrue
+	} else {
+		condition.Type = aiGatewayRouteConditionTypeNotAccepted
+		condition.Reason = "ReconciliationFailed"
+		condition.Status = metav1.ConditionFalse
+	}
+
+	// Search for the same Status condition and replace it, append if not found.
+	found := false
+	for i, cond := range route.Status.Conditions {
+		if cond.Type == condition.Type {
+			found = true
+			route.Status.Conditions[i] = condition
+			break
+		}
+	}
+
+	if !found {
+		route.Status.Conditions = append(route.Status.Conditions, condition)
+	}
+	// And sort the conditions by LastTransitionTime.
+	sort.Slice(route.Status.Conditions, func(i, j int) bool {
+		return route.Status.Conditions[i].LastTransitionTime.Before(&route.Status.Conditions[j].LastTransitionTime)
+	})
+
+	if err := c.client.Status().Update(ctx, route); err != nil {
+		c.logger.Error(err, "failed to update AIGatewayRoute status")
+	}
+}
diff --git a/manifests/charts/ai-gateway-helm/crds/aigateway.envoyproxy.io_aigatewayroutes.yaml b/manifests/charts/ai-gateway-helm/crds/aigateway.envoyproxy.io_aigatewayroutes.yaml
index 752a324..bbc14ac 100644
--- a/manifests/charts/ai-gateway-helm/crds/aigateway.envoyproxy.io_aigatewayroutes.yaml
+++ b/manifests/charts/ai-gateway-helm/crds/aigateway.envoyproxy.io_aigatewayroutes.yaml
@@ -462,6 +462,70 @@ spec:
             - schema
             - targetRefs
             type: object
+          status:
+            description: Status defines the status details of the AIGatewayRoute.
+            properties:
+              conditions:
+                description: Conditions is the list of conditions by the reconciliation
+                  result.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
+            type: object
         type: object
     served: true
     storage: true
+    subresources:
+      status: {}
diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx
index a921872..d2f8b5f 100644
--- a/site/docs/api/api.mdx
+++ b/site/docs/api/api.mdx
@@ -82,6 +82,11 @@ Gateway to patch the generated resources. For example, you can insert a custom f
   type="[AIGatewayRouteSpec](#aigatewayroutespec)"
   required="true"
   description="Spec defines the details of the AIGatewayRoute."
+/><ApiField
+  name="status"
+  type="[AIGatewayRouteStatus](#aigatewayroutestatus)"
+  required="true"
+  description="Status defines the status details of the AIGatewayRoute."
 />
 
 
@@ -287,6 +292,7 @@ BackendSecurityPolicyList contains a list of BackendSecurityPolicy
 - [AIGatewayRouteRuleBackendRef](#aigatewayrouterulebackendref)
 - [AIGatewayRouteRuleMatch](#aigatewayrouterulematch)
 - [AIGatewayRouteSpec](#aigatewayroutespec)
+- [AIGatewayRouteStatus](#aigatewayroutestatus)
 - [AIServiceBackendSpec](#aiservicebackendspec)
 - [APISchema](#apischema)
 - [AWSCredentialsFile](#awscredentialsfile)
@@ -492,6 +498,27 @@ AIGatewayRouteSpec details the AIGatewayRoute configuration.
 />
 
 
+#### AIGatewayRouteStatus
+
+
+
+**Appears in:**
+- [AIGatewayRoute](#aigatewayroute)
+
+AIGatewayRouteStatus contains the conditions by the reconciliation result.
+
+##### Fields
+
+
+
+<ApiField
+  name="conditions"
+  type="[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#condition-v1-meta) array"
+  required="true"
+  description="Conditions is the list of conditions by the reconciliation result."
+/>
+
+
 #### AIServiceBackendSpec
 
 


Test patch:
diff --git a/internal/controller/ai_gateway_route_test.go b/internal/controller/ai_gateway_route_test.go
index 03f0964..25d60aa 100644
--- a/internal/controller/ai_gateway_route_test.go
+++ b/internal/controller/ai_gateway_route_test.go
@@ -239,7 +239,10 @@ func Test_applyExtProcDeploymentConfigUpdate(t *testing.T) {
 }
 
 func requireNewFakeClientWithIndexes(t *testing.T) client.Client {
-	builder := fake.NewClientBuilder().WithScheme(scheme)
+	builder := fake.NewClientBuilder().WithScheme(scheme).
+		WithStatusSubresource(&aigv1a1.AIGatewayRoute{}).
+		WithStatusSubresource(&aigv1a1.AIServiceBackend{}).
+		WithStatusSubresource(&aigv1a1.BackendSecurityPolicy{})
 	err := ApplyIndexing(t.Context(), func(_ context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error {
 		builder = builder.WithIndex(obj, field, extractValue)
 		return nil
@@ -1081,3 +1084,51 @@ func TestAIGatewayRouteController_AnnotateExtProcPods(t *testing.T) {
 		require.Equal(t, uuid, pod.Annotations[extProcConfigAnnotationKey])
 	}
 }
+
+func TestAIGatewayRouteController_updateAIGatewayRouteStatus(t *testing.T) {
+	fakeClient := requireNewFakeClientWithIndexes(t)
+	kube := fake2.NewClientset()
+	s := NewAIGatewayRouteController(fakeClient, kube, logr.Discard(), "foo", "debug")
+
+	r := &aigv1a1.AIGatewayRoute{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "route1",
+			Namespace: "default",
+		},
+	}
+	err := s.client.Create(t.Context(), r, &client.CreateOptions{})
+	require.NoError(t, err)
+
+	s.updateAIGatewayRouteStatus(t.Context(), r, false, "err1")
+
+	var updatedRoute aigv1a1.AIGatewayRoute
+	err = s.client.Get(t.Context(), client.ObjectKey{Name: "route1", Namespace: "default"}, &updatedRoute)
+	require.NoError(t, err)
+	require.Len(t, updatedRoute.Status.Conditions, 1)
+	require.Equal(t, "err1", updatedRoute.Status.Conditions[0].Message)
+	require.Equal(t, aiGatewayRouteConditionTypeNotAccepted, updatedRoute.Status.Conditions[0].Type)
+
+	s.updateAIGatewayRouteStatus(t.Context(), &updatedRoute, true, "ok1")
+	err = s.client.Get(t.Context(), client.ObjectKey{Name: "route1", Namespace: "default"}, &updatedRoute)
+	require.NoError(t, err)
+	require.Len(t, updatedRoute.Status.Conditions, 2)
+	require.Equal(t, "err1", updatedRoute.Status.Conditions[0].Message)
+	require.Equal(t, aiGatewayRouteConditionTypeNotAccepted, updatedRoute.Status.Conditions[0].Type)
+	require.Equal(t, "ok1", updatedRoute.Status.Conditions[1].Message)
+	require.Equal(t, aiGatewayRouteConditionTypeAccepted, updatedRoute.Status.Conditions[1].Type)
+
+	s.updateAIGatewayRouteStatus(t.Context(), &updatedRoute, false, "err2")
+	err = s.client.Get(t.Context(), client.ObjectKey{Name: "route1", Namespace: "default"}, &updatedRoute)
+	require.NoError(t, err)
+	require.Len(t, updatedRoute.Status.Conditions, 2)
+
+	s.updateAIGatewayRouteStatus(t.Context(), &updatedRoute, true, "ok2")
+	err = s.client.Get(t.Context(), client.ObjectKey{Name: "route1", Namespace: "default"}, &updatedRoute)
+	require.NoError(t, err)
+	require.Len(t, updatedRoute.Status.Conditions, 2)
+
+	require.Equal(t, "err2", updatedRoute.Status.Conditions[0].Message)
+	require.Equal(t, aiGatewayRouteConditionTypeNotAccepted, updatedRoute.Status.Conditions[0].Type)
+	require.Equal(t, "ok2", updatedRoute.Status.Conditions[1].Message)
+	require.Equal(t, aiGatewayRouteConditionTypeAccepted, updatedRoute.Status.Conditions[1].Type)
+}
diff --git a/tests/controller/controller_test.go b/tests/controller/controller_test.go
index 9b9ff76..5e5aabf 100644
--- a/tests/controller/controller_test.go
+++ b/tests/controller/controller_test.go
@@ -456,6 +456,8 @@ func TestAIGatewayRouteController(t *testing.T) {
 	})
 
 	t.Run("update", func(t *testing.T) {
+		err := c.Get(t.Context(), client.ObjectKey{Name: "myroute", Namespace: "default"}, origin)
+		require.NoError(t, err)
 		newResource := &corev1.ResourceRequirements{
 			Limits: corev1.ResourceList{
 				corev1.ResourceCPU:    resource.MustParse("300m"),
@@ -464,7 +466,7 @@ func TestAIGatewayRouteController(t *testing.T) {
 		}
 		origin.Spec.FilterConfig.ExternalProcessor.Replicas = ptr.To[int32](3)
 		origin.Spec.FilterConfig.ExternalProcessor.Resources = newResource
-		err := c.Update(t.Context(), origin)
+		err = c.Update(t.Context(), origin)
 		require.NoError(t, err)
 
 		var r aigv1a1.AIGatewayRoute
@@ -488,6 +490,16 @@ func TestAIGatewayRouteController(t *testing.T) {
 			return true
 		}, 30*time.Second, 200*time.Millisecond)
 	})
+
+	t.Run("check statuses", func(t *testing.T) {
+		require.Eventually(t, func() bool {
+			var r aigv1a1.AIGatewayRoute
+			err := c.Get(t.Context(), client.ObjectKey{Name: "myroute", Namespace: "default"}, &r)
+			require.NoError(t, err)
+			fmt.Println(r.Status.Conditions)
+			return len(r.Status.Conditions) == 1
+		}, 30*time.Second, 200*time.Millisecond)
+	})
 }
 
 func TestBackendSecurityPolicyController(t *testing.T) {
diff --git a/tests/internal/envtest.go b/tests/internal/envtest.go
index e40a39c..229c2ce 100644
--- a/tests/internal/envtest.go
+++ b/tests/internal/envtest.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"net/http"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"testing"
 
@@ -23,6 +24,8 @@ import (
 	"github.com/envoyproxy/ai-gateway/internal/controller"
 )
 
+const defaultK8sVersion = "1.31.0"
+
 // NewEnvTest creates a new environment for testing the controller package.
 func NewEnvTest(t *testing.T) (c client.Client, cfg *rest.Config, k kubernetes.Interface) {
 	log.SetLogger(zap.New(zap.WriteTo(os.Stderr), zap.UseDevMode(true)))
@@ -33,6 +36,16 @@ func NewEnvTest(t *testing.T) (c client.Client, cfg *rest.Config, k kubernetes.I
 	for _, file := range files {
 		crds = append(crds, filepath.Join(crdPath, file.Name()))
 	}
+	k8sVersion := os.Getenv("ENVTEST_K8S_VERSION")
+	if k8sVersion == "" {
+		k8sVersion = defaultK8sVersion
+	}
+	t.Logf("Using Kubernetes version %s", k8sVersion)
+	setupEnvTestCmd := exec.Command("go", "tool", "setup-envtest", "use", k8sVersion, "-p", "path")
+	output, err := setupEnvTestCmd.Output()
+	require.NoError(t, err, "Failed to setup envtest: %s", output)
+	t.Logf("Using envtest assets from %s", string(output))
+	t.Setenv("KUBEBUILDER_ASSETS", string(output))
 
 	const (
 		egURLBase    = "https://raw.githubusercontent.com/envoyproxy/gateway/refs/tags/v1.3.0/charts/gateway-helm/crds/generated/"