Repository: envoyproxy/gateway
Pull request #5835 (2025-04-27 17:45:56)

Add metrics for topologyInjector webhook
**What type of PR is this?**
<!--
Your PR title should be descriptive, and generally start with type that contains a subsystem name with `()` if necessary
and summary followed by a colon. format `chore/docs/api/feat/fix/refactor/style/test: summary`.
Examples:
* "docs: fix grammar error"
* "feat(translator): add new feature"
* "fix: fix xx bug"
* "chore: change ci & build tools etc"
* "api: add xxx fields in ClientTrafficPolicy"

Before raising a PR, please go through this section of the developer guide, https://gateway.envoyproxy.io/contributions/develop/#raising-a-pr
-->

<!--
NOTE: If your PR contains any API changes (changes under `/api`), we recommend you to separate these API changes into
a new PR, and we will review the API part first. It will save you lots of implementation time if the API get accepted.
-->

**What this PR does / why we need it**:
Folllow-up to https://github.com/envoyproxy/gateway/pull/5352

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #

<!--
For any non-trivial changes, you need to provide a brief description of the changes in the release notes.
Please add the description to the release-notes/current.yaml file and include this file in the PR.
-->
Release Notes: No

jukie: CC @zirain 
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/5835?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
Attention: Patch coverage is `23.07692%` with `10 lines` in your changes missing coverage. Please review.
> Project coverage is 65.32%. Comparing base [(`71b0d74`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/71b0d74fdb9881e080879e5ced311eb86148c324?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`c09c948`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/c09c948792ecf4febadb96f690c25bb96fdeb223?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

| [Files with missing lines](https://app.codecov.io/gh/envoyproxy/gateway/pull/5835?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) | Patch % | Lines |
|---|---|---|
| [internal/provider/kubernetes/topology\_injector.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5835?src=pr&el=tree&filepath=internal%2Fprovider%2Fkubernetes%2Ftopology_injector.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvcHJvdmlkZXIva3ViZXJuZXRlcy90b3BvbG9neV9pbmplY3Rvci5nbw==) | 23.07% | [10 Missing :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5835?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |

:x: Your patch check has failed because the patch coverage (23.07%) is below the target coverage (60.00%). You can increase the patch coverage or adjust the [target](https://docs.codecov.com/docs/commit-status#target) coverage.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #5835      +/-   ##
==========================================
- Coverage   65.38%   65.32%   -0.06%     
==========================================
  Files         220      220              
  Lines       35180    35187       +7     
==========================================
- Hits        23001    22985      -16     
- Misses      10767    10786      +19     
- Partials     1412     1416       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/5835?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
jukie: /retest
jukie: Not sure how to go about adding tests for the metrics change. Is that required for merging?
zirain: > Not sure how to go about adding tests for the metrics change. Is that required for merging?

it would be better to have, but not a blocker.

it shouldn't be very hard to add in the existing zone e2e.

Patch:
diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go
index 9fbbdbe88..539edbf51 100644
--- a/internal/gatewayapi/route.go
+++ b/internal/gatewayapi/route.go
@@ -1401,6 +1401,11 @@ func validateDestinationSettings(destinationSettings *ir.DestinationSetting, end
 				status.RouteReasonUnsupportedAddressType)
 		}
 	case resource.KindService, resource.KindServiceImport:
+		if endpointRoutingDisabled && isHeadlessService(destinationSettings) {
+			return status.NewRouteStatusError(
+				fmt.Errorf("service %s is a headless Service, please set routingType=Endpoint", destinationSettings.Name),
+				status.RouteReasonUnsupportedSetting)
+		}
 		if !endpointRoutingDisabled && destinationSettings.AddressType != nil && *destinationSettings.AddressType == ir.MIXED {
 			return status.NewRouteStatusError(
 				fmt.Errorf("mixed endpointslice address type for the same backendRef is not supported"),
@@ -1411,6 +1416,17 @@ func validateDestinationSettings(destinationSettings *ir.DestinationSetting, end
 	return nil
 }
 
+// isHeadlessService reports true when any DestinationEndpoint corresponds to
+// a headless Kubernetes Service (ClusterIP="None").
+func isHeadlessService(ds *ir.DestinationSetting) bool {
+	for _, ep := range ds.Endpoints {
+		if ep.Host == "None" {
+			return true
+		}
+	}
+	return false
+}
+
 func (t *Translator) processServiceImportDestinationSetting(
 	name string,
 	backendRef gwapiv1.BackendObjectReference,
diff --git a/internal/provider/kubernetes/metrics.go b/internal/provider/kubernetes/metrics.go
index 0fe6b156b..859df58b0 100644
--- a/internal/provider/kubernetes/metrics.go
+++ b/internal/provider/kubernetes/metrics.go
@@ -20,6 +20,11 @@ var (
 	)
 
 	kindLabel = metrics.NewLabel("kind")
+
+	topologyInjectorEventsTotal = metrics.NewCounter(
+		"topology_injector_webhook_events_total",
+		"Total number of topology injector webhook events.",
+	)
 )
 
 const (
diff --git a/internal/provider/kubernetes/topology_injector.go b/internal/provider/kubernetes/topology_injector.go
index 39d2c3c6d..1d5215909 100644
--- a/internal/provider/kubernetes/topology_injector.go
+++ b/internal/provider/kubernetes/topology_injector.go
@@ -18,6 +18,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
 	"github.com/envoyproxy/gateway/internal/gatewayapi"
+	"github.com/envoyproxy/gateway/internal/metrics"
 )
 
 type ProxyTopologyInjector struct {
@@ -28,11 +29,13 @@ type ProxyTopologyInjector struct {
 func (m *ProxyTopologyInjector) Handle(ctx context.Context, req admission.Request) admission.Response {
 	binding := &corev1.Binding{}
 	if err := m.Decoder.Decode(req, binding); err != nil {
-		klog.Error(err, "decoding binding failed")
+		klog.Error(err, "decoding binding failed", "request.ObjectKind", req.Object.Object.GetObjectKind())
+		topologyInjectorEventsTotal.WithFailure(metrics.ReasonError).Increment()
 		return admission.Errored(http.StatusInternalServerError, err)
 	}
 
 	if binding.Target.Name == "" {
+		topologyInjectorEventsTotal.WithStatus(statusNoAction).Increment()
 		return admission.Allowed("skipped")
 	}
 
@@ -43,13 +46,15 @@ func (m *ProxyTopologyInjector) Handle(ctx context.Context, req admission.Reques
 
 	pod := &corev1.Pod{}
 	if err := m.Get(ctx, podName, pod); err != nil {
-		klog.Error(err, "get pod failed")
+		klog.Error(err, "get pod failed", "pod", podName.String())
+		topologyInjectorEventsTotal.WithFailure(metrics.ReasonError).Increment()
 		return admission.Errored(http.StatusInternalServerError, err)
 	}
 
 	// Skip non-proxy pods
 	if !hasEnvoyProxyLabels(pod.Labels) {
-		klog.Info("skipping pod due to missing labels", "pod", podName)
+		klog.V(1).Info("skipping pod due to missing labels", "pod", podName)
+		topologyInjectorEventsTotal.WithStatus(statusNoAction).Increment()
 		return admission.Allowed("skipped")
 	}
 
@@ -58,7 +63,8 @@ func (m *ProxyTopologyInjector) Handle(ctx context.Context, req admission.Reques
 	}
 	node := &corev1.Node{}
 	if err := m.Get(ctx, nodeName, node); err != nil {
-		klog.Error(err, "get node failed")
+		klog.Error(err, "get node failed", "node", node.Name)
+		topologyInjectorEventsTotal.WithFailure(metrics.ReasonError).Increment()
 		return admission.Errored(http.StatusInternalServerError, err)
 	}
 
@@ -69,10 +75,12 @@ func (m *ProxyTopologyInjector) Handle(ctx context.Context, req admission.Reques
 
 	rawPatch := client.RawPatch(types.JSONPatchType, []byte(patch))
 	if err := m.Patch(ctx, pod, rawPatch); err != nil {
-		klog.Error(err, "patch pod failed")
+		klog.Error(err, "patch pod failed", "pod", podName.String())
+		topologyInjectorEventsTotal.WithFailure(metrics.ReasonError).Increment()
 		return admission.Errored(http.StatusInternalServerError, err)
 	}
-	klog.Info("patch pod succeeded", "pod", podName)
+	klog.V(1).Info("patch pod succeeded", "pod", podName.String())
+	topologyInjectorEventsTotal.WithSuccess().Increment()
 	return admission.Allowed("pod patched")
 }
 
diff --git a/site/content/en/v1.3/tasks/operations/standalone-deployment-mode.md b/site/content/en/v1.3/tasks/operations/standalone-deployment-mode.md
index 102ad3323..1adbe723c 100644
--- a/site/content/en/v1.3/tasks/operations/standalone-deployment-mode.md
+++ b/site/content/en/v1.3/tasks/operations/standalone-deployment-mode.md
@@ -31,14 +31,14 @@ Create a local directory just for testing:
 mkdir -p /tmp/envoy-gateway-test
 ```
 
-As we do not provide the Envoy Gateway binary in latest release,
-you can compile this binary on your own from project by using command:
+You can download the Envoy Gateway binary from [Github release](https://github.com/envoyproxy/gateway/releases/tag/{{< yaml-version >}}),
+or compile the binary on your own from project by using command:
 
 ```shell
 make build
 ```
 
-The compiled binary lies in `bin/{os}/{arch}/envoy-gateway`.
+The compiled binary lies in `{project-home}/bin/{os}/{arch}/envoy-gateway`.
 
 ### Create Certificates
 


Test patch:
diff --git a/internal/gatewayapi/route_test.go b/internal/gatewayapi/route_test.go
index 763450397..131e67879 100644
--- a/internal/gatewayapi/route_test.go
+++ b/internal/gatewayapi/route_test.go
@@ -14,7 +14,10 @@ import (
 	discoveryv1 "k8s.io/api/discovery/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/ptr"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 
+	"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
+	"github.com/envoyproxy/gateway/internal/gatewayapi/status"
 	"github.com/envoyproxy/gateway/internal/ir"
 )
 
@@ -226,3 +229,98 @@ func TestGetIREndpointsFromEndpointSlices(t *testing.T) {
 		})
 	}
 }
+
+func TestValidateDestinationSettings(t *testing.T) {
+	svcKind := gwapiv1.Kind(resource.KindService)
+
+	tests := []struct {
+		name                    string
+		ds                      *ir.DestinationSetting
+		endpointRoutingDisabled bool
+		kind                    *gwapiv1.Kind
+		wantErr                 bool
+		wantReason              gwapiv1.RouteConditionReason
+	}{
+		{
+			name: "headless service rejected when endpointRoutingDisabled=true",
+			ds: &ir.DestinationSetting{
+				Name:      "headless",
+				Endpoints: []*ir.DestinationEndpoint{{Host: "None"}},
+			},
+			endpointRoutingDisabled: true,
+			kind:                    &svcKind,
+			wantErr:                 true,
+			wantReason:              status.RouteReasonUnsupportedSetting,
+		},
+		{
+			name: "normal service allowed with ClusterIP routing",
+			ds: &ir.DestinationSetting{
+				Name:      "normal",
+				Endpoints: []*ir.DestinationEndpoint{{Host: "10.0.0.1"}},
+			},
+			endpointRoutingDisabled: true,
+			kind:                    &svcKind,
+			wantErr:                 false,
+		},
+		{
+			name: "mixed address type rejected when EndpointSlice routing",
+			ds: &ir.DestinationSetting{
+				Name:        "mixed",
+				Endpoints:   []*ir.DestinationEndpoint{{Host: "10.0.0.1"}},
+				AddressType: ptr.To(ir.MIXED),
+			},
+			endpointRoutingDisabled: false,
+			kind:                    &svcKind,
+			wantErr:                 true,
+			wantReason:              status.RouteReasonUnsupportedAddressType,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := validateDestinationSettings(tt.ds, tt.endpointRoutingDisabled, tt.kind)
+			if tt.wantErr {
+				require.Error(t, err)
+				require.Equal(t, tt.wantReason, err.Reason())
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestIsHeadlessService(t *testing.T) {
+	tests := []struct {
+		name      string
+		endpoints []*ir.DestinationEndpoint
+		want      bool
+	}{
+		{
+			name: "headless Service",
+			endpoints: []*ir.DestinationEndpoint{
+				{Host: "None"},
+			},
+			want: true,
+		},
+		{
+			name: "non headless Service with valid ClusterIP",
+			endpoints: []*ir.DestinationEndpoint{
+				{Host: "10.0.0.1"},
+			},
+			want: false,
+		},
+		{
+			name:      "empty slice",
+			endpoints: nil,
+			want:      false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ds := &ir.DestinationSetting{Endpoints: tt.endpoints}
+			got := isHeadlessService(ds)
+			require.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/internal/gatewayapi/testdata/httproute-with-headless-service-endpoint-routing.in.yaml b/internal/gatewayapi/testdata/httproute-with-headless-service-endpoint-routing.in.yaml
new file mode 100644
index 000000000..f8894d999
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-headless-service-endpoint-routing.in.yaml
@@ -0,0 +1,47 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-headless
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+    rules:
+    - backendRefs:
+      - name: headless-service
+        port: 80
+services:
+- apiVersion: v1
+  kind: Service
+  metadata:
+    namespace: default
+    name: headless-service
+  spec:
+    clusterIP: None
+    ports:
+    - port: 80
+envoyProxyForGatewayClass:
+  apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: EnvoyProxy
+  metadata:
+    name: default
+    namespace: default
+  spec:
+    routingType: Endpoint
diff --git a/internal/gatewayapi/testdata/httproute-with-headless-service-endpoint-routing.out.yaml b/internal/gatewayapi/testdata/httproute-with-headless-service-endpoint-routing.out.yaml
new file mode 100644
index 000000000..062854db6
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-headless-service-endpoint-routing.out.yaml
@@ -0,0 +1,135 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-headless
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+    rules:
+    - backendRefs:
+      - name: headless-service
+        port: 80
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      config:
+        apiVersion: gateway.envoyproxy.io/v1alpha1
+        kind: EnvoyProxy
+        metadata:
+          creationTimestamp: null
+          name: default
+          namespace: default
+        spec:
+          logging: {}
+          routingType: Endpoint
+        status: {}
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - directResponse:
+          statusCode: 500
+        hostname: '*'
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-headless
+          namespace: default
+        name: httproute/default/httproute-headless/rule/0/match/-1/*
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/gatewayapi/testdata/httproute-with-headless-service-service-routing.in.yaml b/internal/gatewayapi/testdata/httproute-with-headless-service-service-routing.in.yaml
new file mode 100644
index 000000000..24f19097a
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-headless-service-service-routing.in.yaml
@@ -0,0 +1,47 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-headless
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+    rules:
+    - backendRefs:
+      - name: headless-service
+        port: 80
+services:
+- apiVersion: v1
+  kind: Service
+  metadata:
+    namespace: default
+    name: headless-service
+  spec:
+    clusterIP: None
+    ports:
+    - port: 80
+envoyProxyForGatewayClass:
+  apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: EnvoyProxy
+  metadata:
+    name: default
+    namespace: default
+  spec:
+    routingType: Service
diff --git a/internal/gatewayapi/testdata/httproute-with-headless-service-service-routing.out.yaml b/internal/gatewayapi/testdata/httproute-with-headless-service-service-routing.out.yaml
new file mode 100644
index 000000000..0643b11e0
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-headless-service-service-routing.out.yaml
@@ -0,0 +1,136 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-headless
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+    rules:
+    - backendRefs:
+      - name: headless-service
+        port: 80
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: 'Failed to process route rule 0 backendRef 0: service httproute/default/httproute-headless/rule/0/backend/0
+          is a headless Service, please set routingType=Endpoint.'
+        reason: UnsupportedValue
+        status: "False"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      config:
+        apiVersion: gateway.envoyproxy.io/v1alpha1
+        kind: EnvoyProxy
+        metadata:
+          creationTimestamp: null
+          name: default
+          namespace: default
+        spec:
+          logging: {}
+          routingType: Service
+        status: {}
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - directResponse:
+          statusCode: 500
+        hostname: '*'
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-headless
+          namespace: default
+        name: httproute/default/httproute-headless/rule/0/match/-1/*
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/site/content/en/latest/tasks/observability/gateway-exported-metrics.md b/site/content/en/latest/tasks/observability/gateway-exported-metrics.md
index 07034fc03..9df7bc0ee 100644
--- a/site/content/en/latest/tasks/observability/gateway-exported-metrics.md
+++ b/site/content/en/latest/tasks/observability/gateway-exported-metrics.md
@@ -94,5 +94,14 @@ Envoy Gateway monitors the status of Wasm remote fetch cache.
 
 For metric `wasm_cache_lookup_total`, we are using `hit` label (boolean) to indicate whether the Wasm cache has been hit.
 
+## Topology Injector MutatingWebhookConfiguration
+
+Envoy Gateway monitors the status of the TopologyInjector webhook which injects node topology information to EnvoyProxy pods.
+
+| Name                                     | Description                                       |
+|------------------------------------------|---------------------------------------------------|
+| `topology_injector_webhook_events_total` | Total number of topology injector webhook events. |
+
+This metrics provides information on whether a webhook request was successful, failed, or a no-op.
 
 [prom-format]: https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
diff --git a/site/content/en/latest/tasks/operations/standalone-deployment-mode.md b/site/content/en/latest/tasks/operations/standalone-deployment-mode.md
index 102ad3323..1adbe723c 100644
--- a/site/content/en/latest/tasks/operations/standalone-deployment-mode.md
+++ b/site/content/en/latest/tasks/operations/standalone-deployment-mode.md
@@ -31,14 +31,14 @@ Create a local directory just for testing:
 mkdir -p /tmp/envoy-gateway-test
 ```
 
-As we do not provide the Envoy Gateway binary in latest release,
-you can compile this binary on your own from project by using command:
+You can download the Envoy Gateway binary from [Github release](https://github.com/envoyproxy/gateway/releases/tag/{{< yaml-version >}}),
+or compile the binary on your own from project by using command:
 
 ```shell
 make build
 ```
 
-The compiled binary lies in `bin/{os}/{arch}/envoy-gateway`.
+The compiled binary lies in `{project-home}/bin/{os}/{arch}/envoy-gateway`.
 
 ### Create Certificates