Repository: envoyproxy/gateway

Issue:
HPA not removed after enable and disable.
*Description*:
>What issue is being seen? Describe what should be happening instead of
the bug, for example: The expected value isn't returned, etc.   

HPA will not removed after I enable and disable it in envoyproxy resource , which leading to mismatches with the gateway's replica count.

*Repro steps*:
> Include sample requests, environment, etc. All data and inputs
required to reproduce the bug.
1. Deployment a gateway resource, with nil hpa config in envoyproxy.
2. Enable hpa in envoyproxy.
3. Delete hpa config in envoyproxy.
4. HPA resource still exists.

*Environment*:
EG v1.3.0


Hints from issue comments:
@arkodg: we need to triage why this code isnt kicking in https://github.com/envoyproxy/gateway/blob/98d6314530ed95f775feb4306a431226cea97be3/internal/infrastructure/kubernetes/infra_resource.go#L324 which should be cleaning up hpa@MaYuan-02: @arkodg  Found it! This PR may be the root cause. If hpa config is nil, createOrUpdateHPA will do nothing and return directly.
https://github.com/envoyproxy/gateway/pull/4362
@arkodg: thanks for finding this @MaYuan-02 , we probably need to revert that change
cc @qicz @arkodg: closing in favor of https://github.com/envoyproxy/gateway/pull/5737

Solution patch:
diff --git a/internal/infrastructure/kubernetes/infra.go b/internal/infrastructure/kubernetes/infra.go
index 82d51d066..2b9916f25 100644
--- a/internal/infrastructure/kubernetes/infra.go
+++ b/internal/infrastructure/kubernetes/infra.go
@@ -36,13 +36,9 @@ type ResourceRender interface {
 	Service() (*corev1.Service, error)
 	ConfigMap() (*corev1.ConfigMap, error)
 	Deployment() (*appsv1.Deployment, error)
-	DeploymentSpec() (*egv1a1.KubernetesDeploymentSpec, error)
 	DaemonSet() (*appsv1.DaemonSet, error)
-	DaemonSetSpec() (*egv1a1.KubernetesDaemonSetSpec, error)
 	HorizontalPodAutoscaler() (*autoscalingv2.HorizontalPodAutoscaler, error)
-	HorizontalPodAutoscalerSpec() (*egv1a1.KubernetesHorizontalPodAutoscalerSpec, error)
 	PodDisruptionBudget() (*policyv1.PodDisruptionBudget, error)
-	PodDisruptionBudgetSpec() (*egv1a1.KubernetesPodDisruptionBudgetSpec, error)
 }
 
 // Infra manages the creation and deletion of Kubernetes infrastructure
diff --git a/internal/infrastructure/kubernetes/infra_resource.go b/internal/infrastructure/kubernetes/infra_resource.go
index 6c2cacc2b..37c5d35fd 100644
--- a/internal/infrastructure/kubernetes/infra_resource.go
+++ b/internal/infrastructure/kubernetes/infra_resource.go
@@ -91,11 +91,6 @@ func (i *Infra) createOrUpdateConfigMap(ctx context.Context, r ResourceRender) (
 // createOrUpdateDeployment creates a Deployment in the kube api server based on the provided
 // ResourceRender, if it doesn't exist and updates it if it does.
 func (i *Infra) createOrUpdateDeployment(ctx context.Context, r ResourceRender) (err error) {
-	// If deployment config is nil,ignore Deployment.
-	if deploymentConfig, er := r.DeploymentSpec(); deploymentConfig == nil {
-		return er
-	}
-
 	var (
 		deployment *appsv1.Deployment
 		startTime  = time.Now()
@@ -183,11 +178,6 @@ func (i *Infra) createOrUpdateDeployment(ctx context.Context, r ResourceRender)
 // createOrUpdateDaemonSet creates a DaemonSet in the kube api server based on the provided
 // ResourceRender, if it doesn't exist and updates it if it does.
 func (i *Infra) createOrUpdateDaemonSet(ctx context.Context, r ResourceRender) (err error) {
-	// If daemonset config is nil, ignore DaemonSet.
-	if daemonSetConfig, er := r.DaemonSetSpec(); daemonSetConfig == nil {
-		return er
-	}
-
 	var (
 		daemonSet *appsv1.DaemonSet
 		startTime = time.Now()
@@ -281,11 +271,6 @@ func isSelectorMatch(labelselector *metav1.LabelSelector, l map[string]string) (
 }
 
 func (i *Infra) createOrUpdatePodDisruptionBudget(ctx context.Context, r ResourceRender) (err error) {
-	// If podDisruptionBudget config is nil or MinAvailable is nil, ignore PodDisruptionBudget.
-	if podDisruptionBudget, er := r.PodDisruptionBudgetSpec(); podDisruptionBudget == nil {
-		return er
-	}
-
 	var (
 		pdb       *policyv1.PodDisruptionBudget
 		startTime = time.Now()
@@ -323,11 +308,6 @@ func (i *Infra) createOrUpdatePodDisruptionBudget(ctx context.Context, r Resourc
 // the provided ResourceRender, if it doesn't exist and updates it if it does,
 // and delete hpa if not set.
 func (i *Infra) createOrUpdateHPA(ctx context.Context, r ResourceRender) (err error) {
-	// If hpa config is nil, ignore HorizontalPodAutoscaler.
-	if hpaConfig, er := r.HorizontalPodAutoscalerSpec(); hpaConfig == nil {
-		return er
-	}
-
 	var (
 		hpa       *autoscalingv2.HorizontalPodAutoscaler
 		startTime = time.Now()
@@ -439,11 +419,6 @@ func (i *Infra) deleteServiceAccount(ctx context.Context, r ResourceRender) (err
 
 // deleteDeployment deletes the Envoy Deployment in the kube api server, if it exists.
 func (i *Infra) deleteDeployment(ctx context.Context, r ResourceRender) (err error) {
-	// If deployment config is nil,ignore Deployment.
-	if deploymentConfig, er := r.DeploymentSpec(); deploymentConfig == nil {
-		return er
-	}
-
 	var (
 		name, ns   = r.Name(), i.Namespace
 		deployment = &appsv1.Deployment{
@@ -479,11 +454,6 @@ func (i *Infra) deleteDeployment(ctx context.Context, r ResourceRender) (err err
 
 // deleteDaemonSet deletes the Envoy DaemonSet in the kube api server, if it exists.
 func (i *Infra) deleteDaemonSet(ctx context.Context, r ResourceRender) (err error) {
-	// If daemonset config is nil, ignore DaemonSet.
-	if daemonSetConfig, er := r.DaemonSetSpec(); daemonSetConfig == nil {
-		return er
-	}
-
 	var (
 		name, ns  = r.Name(), i.Namespace
 		daemonSet = &appsv1.DaemonSet{
@@ -589,11 +559,6 @@ func (i *Infra) deleteService(ctx context.Context, r ResourceRender) (err error)
 
 // deleteHpa deletes the Horizontal Pod Autoscaler associated to its renderer, if it exists.
 func (i *Infra) deleteHPA(ctx context.Context, r ResourceRender) (err error) {
-	// If hpa config is nil, ignore HorizontalPodAutoscaler.
-	if hpaConfig, er := r.HorizontalPodAutoscalerSpec(); hpaConfig == nil {
-		return er
-	}
-
 	var (
 		name, ns = r.Name(), i.Namespace
 		hpa      = &autoscalingv2.HorizontalPodAutoscaler{
@@ -629,11 +594,6 @@ func (i *Infra) deleteHPA(ctx context.Context, r ResourceRender) (err error) {
 
 // deletePDB deletes the PodDistribution budget associated to its renderer, if it exists.
 func (i *Infra) deletePDB(ctx context.Context, r ResourceRender) (err error) {
-	// If podDisruptionBudget config is nil or MinAvailable is nil, ignore PodDisruptionBudget.
-	if podDisruptionBudget, er := r.PodDisruptionBudgetSpec(); podDisruptionBudget == nil {
-		return er
-	}
-
 	var (
 		name, ns = r.Name(), i.Namespace
 		pdb      = &policyv1.PodDisruptionBudget{
diff --git a/internal/infrastructure/kubernetes/proxy/resource_provider.go b/internal/infrastructure/kubernetes/proxy/resource_provider.go
index daa318c2f..6c895c75c 100644
--- a/internal/infrastructure/kubernetes/proxy/resource_provider.go
+++ b/internal/infrastructure/kubernetes/proxy/resource_provider.go
@@ -251,8 +251,8 @@ func (r *ResourceRender) stableSelector() *metav1.LabelSelector {
 	return resource.GetSelector(envoyLabels(labels))
 }
 
-// DeploymentSpec returns the `Deployment` sets spec.
-func (r *ResourceRender) DeploymentSpec() (*egv1a1.KubernetesDeploymentSpec, error) {
+// Deployment returns the expected Deployment based on the provided infra.
+func (r *ResourceRender) Deployment() (*appsv1.Deployment, error) {
 	proxyConfig := r.infra.GetProxyConfig()
 
 	// Get the EnvoyProxy config to configure the deployment.
@@ -260,21 +260,13 @@ func (r *ResourceRender) DeploymentSpec() (*egv1a1.KubernetesDeploymentSpec, err
 	if provider.Type != egv1a1.ProviderTypeKubernetes {
 		return nil, fmt.Errorf("invalid provider type %v for Kubernetes infra manager", provider.Type)
 	}
-
 	deploymentConfig := provider.GetEnvoyProxyKubeProvider().EnvoyDeployment
 
-	return deploymentConfig, nil
-}
-
-// Deployment returns the expected Deployment based on the provided infra.
-func (r *ResourceRender) Deployment() (*appsv1.Deployment, error) {
-	deploymentConfig, er := r.DeploymentSpec()
-	// If deployment config is nil,ignore Deployment.
+	// If deployment config is nil, it's not Deployment installation.
 	if deploymentConfig == nil {
-		return nil, er
+		return nil, nil
 	}
 
-	proxyConfig := r.infra.GetProxyConfig()
 	// Get expected bootstrap configurations rendered ProxyContainers
 	containers, err := expectedProxyContainers(r.infra, deploymentConfig.Container, proxyConfig.Spec.Shutdown, r.ShutdownManager, r.Namespace, r.DNSDomain)
 	if err != nil {
@@ -348,8 +340,7 @@ func (r *ResourceRender) Deployment() (*appsv1.Deployment, error) {
 	return deployment, nil
 }
 
-// DaemonSetSpec returns the `DaemonSet` sets spec.
-func (r *ResourceRender) DaemonSetSpec() (*egv1a1.KubernetesDaemonSetSpec, error) {
+func (r *ResourceRender) DaemonSet() (*appsv1.DaemonSet, error) {
 	proxyConfig := r.infra.GetProxyConfig()
 
 	// Get the EnvoyProxy config to configure the daemonset.
@@ -358,18 +349,13 @@ func (r *ResourceRender) DaemonSetSpec() (*egv1a1.KubernetesDaemonSetSpec, error
 		return nil, fmt.Errorf("invalid provider type %v for Kubernetes infra manager", provider.Type)
 	}
 
-	return provider.GetEnvoyProxyKubeProvider().EnvoyDaemonSet, nil
-}
+	daemonSetConfig := provider.GetEnvoyProxyKubeProvider().EnvoyDaemonSet
 
-func (r *ResourceRender) DaemonSet() (*appsv1.DaemonSet, error) {
-	daemonSetConfig, err := r.DaemonSetSpec()
-	// If daemonset config is nil, ignore DaemonSet.
+	// If daemonset config is nil, it's not DaemonSet installation.
 	if daemonSetConfig == nil {
-		return nil, err
+		return nil, nil
 	}
 
-	proxyConfig := r.infra.GetProxyConfig()
-
 	// Get expected bootstrap configurations rendered ProxyContainers
 	containers, err := expectedProxyContainers(r.infra, daemonSetConfig.Container, proxyConfig.Spec.Shutdown, r.ShutdownManager, r.Namespace, r.DNSDomain)
 	if err != nil {
@@ -424,8 +410,7 @@ func (r *ResourceRender) DaemonSet() (*appsv1.DaemonSet, error) {
 	return daemonSet, nil
 }
 
-// PodDisruptionBudgetSpec returns the `PodDisruptionBudget` sets spec.
-func (r *ResourceRender) PodDisruptionBudgetSpec() (*egv1a1.KubernetesPodDisruptionBudgetSpec, error) {
+func (r *ResourceRender) pdbConfig() (*egv1a1.KubernetesPodDisruptionBudgetSpec, error) {
 	provider := r.infra.GetProxyConfig().GetEnvoyProxyProvider()
 	if provider.Type != egv1a1.ProviderTypeKubernetes {
 		return nil, fmt.Errorf("invalid provider type %v for Kubernetes infra manager", provider.Type)
@@ -440,9 +425,9 @@ func (r *ResourceRender) PodDisruptionBudgetSpec() (*egv1a1.KubernetesPodDisrupt
 }
 
 func (r *ResourceRender) PodDisruptionBudget() (*policyv1.PodDisruptionBudget, error) {
-	podDisruptionBudgetConfig, err := r.PodDisruptionBudgetSpec()
+	pdb, err := r.pdbConfig()
 	// If podDisruptionBudget config is nil, ignore PodDisruptionBudget.
-	if podDisruptionBudgetConfig == nil {
+	if pdb == nil {
 		return nil, err
 	}
 
@@ -450,10 +435,10 @@ func (r *ResourceRender) PodDisruptionBudget() (*policyv1.PodDisruptionBudget, e
 		Selector: r.stableSelector(),
 	}
 	switch {
-	case podDisruptionBudgetConfig.MinAvailable != nil:
-		pdbSpec.MinAvailable = podDisruptionBudgetConfig.MinAvailable
-	case podDisruptionBudgetConfig.MaxUnavailable != nil:
-		pdbSpec.MaxUnavailable = podDisruptionBudgetConfig.MaxUnavailable
+	case pdb.MinAvailable != nil:
+		pdbSpec.MinAvailable = pdb.MinAvailable
+	case pdb.MaxUnavailable != nil:
+		pdbSpec.MaxUnavailable = pdb.MaxUnavailable
 	default:
 		pdbSpec.MinAvailable = &intstr.IntOrString{Type: intstr.Int, IntVal: 0}
 	}
@@ -471,29 +456,22 @@ func (r *ResourceRender) PodDisruptionBudget() (*policyv1.PodDisruptionBudget, e
 	}
 
 	// apply merge patch to PodDisruptionBudget
-	if podDisruptionBudget, err = podDisruptionBudgetConfig.ApplyMergePatch(podDisruptionBudget); err != nil {
+	if podDisruptionBudget, err = pdb.ApplyMergePatch(podDisruptionBudget); err != nil {
 		return nil, err
 	}
 
 	return podDisruptionBudget, nil
 }
 
-// HorizontalPodAutoscalerSpec returns the `HorizontalPodAutoscaler` sets spec.
-func (r *ResourceRender) HorizontalPodAutoscalerSpec() (*egv1a1.KubernetesHorizontalPodAutoscalerSpec, error) {
+func (r *ResourceRender) HorizontalPodAutoscaler() (*autoscalingv2.HorizontalPodAutoscaler, error) {
 	provider := r.infra.GetProxyConfig().GetEnvoyProxyProvider()
 	if provider.Type != egv1a1.ProviderTypeKubernetes {
 		return nil, fmt.Errorf("invalid provider type %v for Kubernetes infra manager", provider.Type)
 	}
 
 	hpaConfig := provider.GetEnvoyProxyKubeProvider().EnvoyHpa
-	return hpaConfig, nil
-}
-
-func (r *ResourceRender) HorizontalPodAutoscaler() (*autoscalingv2.HorizontalPodAutoscaler, error) {
-	hpaConfig, err := r.HorizontalPodAutoscalerSpec()
-	// If hpa config is nil, ignore HorizontalPodAutoscaler.
 	if hpaConfig == nil {
-		return nil, err
+		return nil, nil
 	}
 
 	hpa := &autoscalingv2.HorizontalPodAutoscaler{
@@ -519,8 +497,6 @@ func (r *ResourceRender) HorizontalPodAutoscaler() (*autoscalingv2.HorizontalPod
 		},
 	}
 
-	provider := r.infra.GetProxyConfig().GetEnvoyProxyProvider()
-
 	// set deployment target ref name
 	deploymentConfig := provider.GetEnvoyProxyKubeProvider().EnvoyDeployment
 	if deploymentConfig.Name != nil {
@@ -529,7 +505,8 @@ func (r *ResourceRender) HorizontalPodAutoscaler() (*autoscalingv2.HorizontalPod
 		hpa.Spec.ScaleTargetRef.Name = r.Name()
 	}
 
-	if hpa, err = hpaConfig.ApplyMergePatch(hpa); err != nil {
+	hpa, err := hpaConfig.ApplyMergePatch(hpa)
+	if err != nil {
 		return nil, err
 	}
 
diff --git a/internal/infrastructure/kubernetes/ratelimit/resource_provider.go b/internal/infrastructure/kubernetes/ratelimit/resource_provider.go
index ff183e459..53e3c8d5a 100644
--- a/internal/infrastructure/kubernetes/ratelimit/resource_provider.go
+++ b/internal/infrastructure/kubernetes/ratelimit/resource_provider.go
@@ -191,18 +191,8 @@ func (r *ResourceRender) ServiceAccount() (*corev1.ServiceAccount, error) {
 	return sa, nil
 }
 
-// DeploymentSpec returns the `Deployment` sets spec.
-func (r *ResourceRender) DeploymentSpec() (*egv1a1.KubernetesDeploymentSpec, error) {
-	return r.rateLimitDeployment, nil
-}
-
 // Deployment returns the expected rate limit Deployment based on the provided infra.
 func (r *ResourceRender) Deployment() (*appsv1.Deployment, error) {
-	// If deployment config is nil,ignore Deployment.
-	if deploymentConfig, er := r.DeploymentSpec(); deploymentConfig == nil {
-		return nil, er
-	}
-
 	containers := expectedRateLimitContainers(r.rateLimit, r.rateLimitDeployment, r.Namespace)
 	selector := resource.GetSelector(rateLimitLabels())
 
@@ -299,11 +289,6 @@ func (r *ResourceRender) Deployment() (*appsv1.Deployment, error) {
 	return deployment, nil
 }
 
-// DaemonSetSpec returns the `DaemonSet` sets spec.
-func (r *ResourceRender) DaemonSetSpec() (*egv1a1.KubernetesDaemonSetSpec, error) {
-	return nil, nil
-}
-
 // TODO: implement this method
 func (r *ResourceRender) DaemonSet() (*appsv1.DaemonSet, error) {
 	return nil, nil
@@ -357,11 +342,6 @@ func (r *ResourceRender) HorizontalPodAutoscaler() (*autoscalingv2.HorizontalPod
 	return hpa, nil
 }
 
-// PodDisruptionBudgetSpec returns the `PodDisruptionBudget` sets spec.
-func (r *ResourceRender) PodDisruptionBudgetSpec() (*egv1a1.KubernetesPodDisruptionBudgetSpec, error) {
-	return nil, nil
-}
-
 func (r *ResourceRender) PodDisruptionBudget() (*policyv1.PodDisruptionBudget, error) {
 	return nil, nil
 }



Test patch:
diff --git a/test/e2e/testdata/envoyproxy-hpa.yaml b/test/e2e/testdata/envoyproxy-hpa.yaml
new file mode 100644
index 000000000..2fbd0ffbe
--- /dev/null
+++ b/test/e2e/testdata/envoyproxy-hpa.yaml
@@ -0,0 +1,50 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: eg-hpa
+  namespace: gateway-conformance-infra
+spec:
+  gatewayClassName: "{GATEWAY_CLASS_NAME}"
+  listeners:
+    - name: http
+      port: 80
+      protocol: HTTP
+      allowedRoutes:
+        namespaces:
+          from: All
+  infrastructure:
+    parametersRef:
+      group: gateway.envoyproxy.io
+      kind: EnvoyProxy
+      name: eg-hpa
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: EnvoyProxy
+metadata:
+  name: eg-hpa
+  namespace: gateway-conformance-infra
+spec:
+  ipFamily: IPv4
+  provider:
+    type: Kubernetes
+    kubernetes:
+      envoyHpa:
+        minReplicas: 1
+        maxReplicas: 2
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: foo-route
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+    - name: eg-hpa
+  rules:
+    - backendRefs:
+        - name: infra-backend-v1
+          port: 8080
+      matches:
+        - path:
+            type: PathPrefix
+            value: /foo
diff --git a/test/e2e/tests/envoyproxy_hpa.go b/test/e2e/tests/envoyproxy_hpa.go
new file mode 100644
index 000000000..4e4a830e0
--- /dev/null
+++ b/test/e2e/tests/envoyproxy_hpa.go
@@ -0,0 +1,68 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+
+package tests
+
+import (
+	"context"
+	"testing"
+
+	"k8s.io/apimachinery/pkg/types"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+	"sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, EnvoyProxyHPATest)
+}
+
+var EnvoyProxyHPATest = suite.ConformanceTest{
+	ShortName:   "EnvoyProxyHPA",
+	Description: "Test running Envoy with HPA",
+	Manifests:   []string{"testdata/envoyproxy-hpa.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		t.Run("RunAndDelete", func(t *testing.T) {
+			ns := "gateway-conformance-infra"
+			routeNN := types.NamespacedName{Name: "foo-route", Namespace: ns}
+			gwNN := types.NamespacedName{Name: "eg-hpa", Namespace: ns}
+			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+			OkResp := http.ExpectedResponse{
+				Request: http.Request{
+					Path: "/foo",
+				},
+				Response: http.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			}
+
+			// Make sure there's a deployment/HPA for the gateway
+			ExpectEnvoyProxyDeploymentCount(t, suite, gwNN, 1)
+			ExpectEnvoyProxyHPACount(t, suite, gwNN, 1)
+
+			// Send a request to a valid path and expect a successful response
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, OkResp)
+
+			// Update the Gateway to without HPA
+			gw := &gwapiv1.Gateway{}
+			err := suite.Client.Get(context.Background(), gwNN, gw)
+			if err != nil {
+				t.Fatalf("Failed to get Gateway: %v", err)
+			}
+			gw.Spec.Infrastructure = nil
+			err = suite.Client.Update(context.Background(), gw)
+			if err != nil {
+				t.Fatalf("Failed to update Gateway: %v", err)
+			}
+
+			ExpectEnvoyProxyDeploymentCount(t, suite, gwNN, 1)
+			ExpectEnvoyProxyHPACount(t, suite, gwNN, 0)
+		})
+	},
+}
diff --git a/test/e2e/tests/utils.go b/test/e2e/tests/utils.go
index f5545df82..123640277 100644
--- a/test/e2e/tests/utils.go
+++ b/test/e2e/tests/utils.go
@@ -28,6 +28,8 @@ import (
 	dto "github.com/prometheus/client_model/go"
 	"github.com/prometheus/common/expfmt"
 	"github.com/stretchr/testify/require"
+	appsv1 "k8s.io/api/apps/v1"
+	autoscalingv2 "k8s.io/api/autoscaling/v2"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
@@ -726,3 +728,47 @@ func ContentEncoding(compressorType egv1a1.CompressorType) string {
 
 	return encoding
 }
+
+func ExpectEnvoyProxyDeploymentCount(t *testing.T, suite *suite.ConformanceTestSuite, gwNN types.NamespacedName, expectedCount int) {
+	err := wait.PollUntilContextTimeout(context.TODO(), time.Second, suite.TimeoutConfig.DeleteTimeout, true, func(ctx context.Context) (bool, error) {
+		deploys := &appsv1.DeploymentList{}
+		err := suite.Client.List(ctx, deploys, &client.ListOptions{
+			Namespace: "envoy-gateway-system",
+			LabelSelector: labels.SelectorFromSet(map[string]string{
+				"app.kubernetes.io/managed-by":                   "envoy-gateway",
+				"app.kubernetes.io/name":                         "envoy",
+				"gateway.envoyproxy.io/owning-gateway-name":      gwNN.Name,
+				"gateway.envoyproxy.io/owning-gateway-namespace": gwNN.Namespace,
+			}),
+		})
+		if err != nil {
+			return false, err
+		}
+
+		return len(deploys.Items) == expectedCount, err
+	})
+	if err != nil {
+		t.Fatalf("Failed to check Deployment count(%d) for the Gateway: %v", expectedCount, err)
+	}
+}
+
+func ExpectEnvoyProxyHPACount(t *testing.T, suite *suite.ConformanceTestSuite, gwNN types.NamespacedName, expectedCount int) {
+	err := wait.PollUntilContextTimeout(context.TODO(), time.Second, suite.TimeoutConfig.DeleteTimeout, true, func(ctx context.Context) (bool, error) {
+		hpa := &autoscalingv2.HorizontalPodAutoscalerList{}
+		err := suite.Client.List(ctx, hpa, &client.ListOptions{
+			Namespace: "envoy-gateway-system",
+			LabelSelector: labels.SelectorFromSet(map[string]string{
+				"gateway.envoyproxy.io/owning-gateway-name":      gwNN.Name,
+				"gateway.envoyproxy.io/owning-gateway-namespace": gwNN.Namespace,
+			}),
+		})
+		if err != nil {
+			return false, err
+		}
+
+		return len(hpa.Items) == 1, err
+	})
+	if err != nil {
+		t.Fatalf("Failed to check HPA count(%d) for the Gateway: %v", expectedCount, err)
+	}
+}