Repository: envoyproxy/gateway
Pull request #202 (2022-08-13 01:56:00)

Adds Initial Envoy Service Support
Adds initial support for provisioning the Envoy Service.

Requires: https://github.com/envoyproxy/gateway/pull/200

Fixes: https://github.com/envoyproxy/gateway/issues/186

codecov-commenter: # [Codecov](https://codecov.io/gh/envoyproxy/gateway/pull/202?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#202](https://codecov.io/gh/envoyproxy/gateway/pull/202?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (69ba853) into [main](https://codecov.io/gh/envoyproxy/gateway/commit/6c4bf9b21723a997e38936aa2c2cb16a97a6bca7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (6c4bf9b) will **increase** coverage by `1.65%`.
> The diff coverage is `75.18%`.

> :exclamation: Current head 69ba853 differs from pull request most recent head f593477. Consider uploading reports for the commit f593477 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #202      +/-   ##
==========================================
+ Coverage   62.04%   63.69%   +1.65%     
==========================================
  Files          25       27       +2     
  Lines        2105     2355     +250     
==========================================
+ Hits         1306     1500     +194     
- Misses        717      768      +51     
- Partials       82       87       +5     
```


| [Impacted Files](https://codecov.io/gh/envoyproxy/gateway/pull/202?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [internal/ir/infra.go](https://codecov.io/gh/envoyproxy/gateway/pull/202/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvaXIvaW5mcmEuZ28=) | `67.50% <25.00%> (-2.63%)` | :arrow_down: |
| [internal/infrastructure/kubernetes/service.go](https://codecov.io/gh/envoyproxy/gateway/pull/202/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvaW5mcmFzdHJ1Y3R1cmUva3ViZXJuZXRlcy9zZXJ2aWNlLmdv) | `46.57% <46.57%> (ø)` | |
| [internal/infrastructure/kubernetes/infra.go](https://codecov.io/gh/envoyproxy/gateway/pull/202/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvaW5mcmFzdHJ1Y3R1cmUva3ViZXJuZXRlcy9pbmZyYS5nbw==) | `58.33% <55.00%> (+19.44%)` | :arrow_up: |
| [internal/infrastructure/kubernetes/deployment.go](https://codecov.io/gh/envoyproxy/gateway/pull/202/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvaW5mcmFzdHJ1Y3R1cmUva3ViZXJuZXRlcy9kZXBsb3ltZW50Lmdv) | `89.33% <89.33%> (ø)` | |
| [internal/gatewayapi/helpers.go](https://codecov.io/gh/envoyproxy/gateway/pull/202/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvZ2F0ZXdheWFwaS9oZWxwZXJzLmdv) | `67.79% <100.00%> (+1.72%)` | :arrow_up: |
| [internal/gatewayapi/translator.go](https://codecov.io/gh/envoyproxy/gateway/pull/202/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvZ2F0ZXdheWFwaS90cmFuc2xhdG9yLmdv) | `86.04% <100.00%> (+0.15%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)


Patch:
diff --git a/go.mod b/go.mod
index 6c4d353e9..746d5ebb9 100644
--- a/go.mod
+++ b/go.mod
@@ -66,6 +66,7 @@ require (
 	github.com/spf13/pflag v1.0.5 // indirect
 	go.uber.org/atomic v1.7.0 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
+	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
 	golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
 	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
 	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
diff --git a/go.sum b/go.sum
index 0978f9682..a2b4eef94 100644
--- a/go.sum
+++ b/go.sum
@@ -550,6 +550,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
+golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go
index d5d853020..d26833221 100644
--- a/internal/gatewayapi/translator.go
+++ b/internal/gatewayapi/translator.go
@@ -3,6 +3,7 @@ package gatewayapi
 import (
 	"fmt"
 
+	"golang.org/x/exp/slices"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/sets"
@@ -135,7 +136,6 @@ type portListeners struct {
 
 func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR *ir.Xds, infraIR *ir.Infra, resources *Resources) {
 	portListenerInfo := map[v1beta1.PortNumber]*portListeners{}
-	infraPorts := map[v1beta1.PortNumber]string{}
 
 	// Iterate through all listeners and collect info about protocols
 	// and hostnames per port.
@@ -166,21 +166,7 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR *ir.Xds,
 			}
 
 			portListenerInfo[listener.Port].hostnames[hostname]++
-
-			// Setup Infra IR listeners.
-			if _, found := infraPorts[listener.Port]; !found {
-				infraPorts[listener.Port] = irInfraPortName(listener)
-			}
-		}
-	}
-
-	for port, name := range infraPorts {
-		// Add the listener to the Infra IR.
-		infraPort := ir.ListenerPort{
-			Name: name,
-			Port: int32(port),
 		}
-		infraIR.Proxy.Listeners[0].Ports = append(infraIR.Proxy.Listeners[0].Ports, infraPort)
 	}
 
 	// Set Conflicted conditions for any listeners with conflicting specs.
@@ -211,6 +197,9 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR *ir.Xds,
 		}
 	}
 
+	// Infra IR proxy ports must be unique.
+	var foundPorts []v1beta1.PortNumber
+
 	// Iterate through all listeners to validate spec
 	// and compute status for each, and add valid ones
 	// to the Xds IR.
@@ -438,6 +427,22 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR *ir.Xds,
 				irListener.Hostnames = append(irListener.Hostnames, string(*listener.Hostname))
 			}
 			xdsIR.HTTP = append(xdsIR.HTTP, irListener)
+
+			// Add the listener to the Infra IR. Infra IR ports must have a unique port number.
+			if !slices.Contains(foundPorts, listener.Port) {
+				foundPorts = append(foundPorts, listener.Port)
+				proto := ir.HTTPProtocolType
+				if listener.Protocol == v1beta1.HTTPSProtocolType {
+					proto = ir.HTTPSProtocolType
+				}
+				infraPort := ir.ListenerPort{
+					Name:     irInfraPortName(listener),
+					Protocol: proto,
+					Port:     int32(listener.Port),
+				}
+				// Only 1 listener is supported.
+				infraIR.Proxy.Listeners[0].Ports = append(infraIR.Proxy.Listeners[0].Ports, infraPort)
+			}
 		}
 	}
 }
diff --git a/internal/infrastructure/kubernetes/deployment.go b/internal/infrastructure/kubernetes/deployment.go
index 72c193fdb..e605a3093 100644
--- a/internal/infrastructure/kubernetes/deployment.go
+++ b/internal/infrastructure/kubernetes/deployment.go
@@ -18,7 +18,7 @@ import (
 const (
 	// envoyContainerName is the name of the Envoy container.
 	envoyContainerName = "envoy"
-	// envoyNsEnvVar is the name of the contour namespace environment variable.
+	// envoyNsEnvVar is the name of the Envoy Gateway namespace environment variable.
 	envoyNsEnvVar = "ENVOY_GATEWAY_NAMESPACE"
 	// envoyPodEnvVar is the name of the Envoy pod name environment variable.
 	envoyPodEnvVar = "ENVOY_POD_NAME"
diff --git a/internal/infrastructure/kubernetes/infra.go b/internal/infrastructure/kubernetes/infra.go
index 34e6ba1bc..e80098a59 100644
--- a/internal/infrastructure/kubernetes/infra.go
+++ b/internal/infrastructure/kubernetes/infra.go
@@ -27,6 +27,7 @@ type Infra struct {
 type Resources struct {
 	ServiceAccount *corev1.ServiceAccount
 	Deployment     *appsv1.Deployment
+	Service        *corev1.Service
 }
 
 // NewInfra returns a new Infra.
@@ -43,6 +44,7 @@ func newResources() *Resources {
 	return &Resources{
 		ServiceAccount: new(corev1.ServiceAccount),
 		Deployment:     new(appsv1.Deployment),
+		Service:        new(corev1.Service),
 	}
 }
 
@@ -60,6 +62,8 @@ func (i *Infra) addResource(obj client.Object) error {
 		i.Resources.ServiceAccount = o
 	case *appsv1.Deployment:
 		i.Resources.Deployment = o
+	case *corev1.Service:
+		i.Resources.Service = o
 	default:
 		return fmt.Errorf("unexpected object kind %s", obj.GetObjectKind())
 	}
@@ -89,5 +93,9 @@ func (i *Infra) CreateInfra(ctx context.Context, infra *ir.Infra) error {
 		return err
 	}
 
+	if err := i.createServiceIfNeeded(ctx, infra); err != nil {
+		return err
+	}
+
 	return nil
 }
diff --git a/internal/infrastructure/kubernetes/service.go b/internal/infrastructure/kubernetes/service.go
new file mode 100644
index 000000000..30fb96645
--- /dev/null
+++ b/internal/infrastructure/kubernetes/service.go
@@ -0,0 +1,118 @@
+package kubernetes
+
+import (
+	"context"
+	"fmt"
+
+	corev1 "k8s.io/api/core/v1"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/intstr"
+
+	"github.com/envoyproxy/gateway/internal/ir"
+)
+
+const (
+	// envoyServiceHTTPPort is the HTTP port number of the Envoy service.
+	envoyServiceHTTPPort = 80
+	// envoyServiceHTTPSPort is the HTTPS port number of the Envoy service.
+	envoyServiceHTTPSPort = 443
+)
+
+// createServiceIfNeeded creates a Service based on the provided infra, if
+// it doesn't exist in the kube api server.
+func (i *Infra) createServiceIfNeeded(ctx context.Context, infra *ir.Infra) error {
+	current, err := i.getService(ctx, infra)
+	if err != nil {
+		if kerrors.IsNotFound(err) {
+			svc, err := i.createService(ctx, infra)
+			if err != nil {
+				return err
+			}
+			if err := i.addResource(svc); err != nil {
+				return err
+			}
+			return nil
+		}
+		return err
+	}
+
+	if err := i.addResource(current); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// getService gets the Service from the kube api for the provided infra.
+func (i *Infra) getService(ctx context.Context, infra *ir.Infra) (*corev1.Service, error) {
+	ns := i.Namespace
+	name := infra.Proxy.ObjectName()
+	key := types.NamespacedName{
+		Namespace: ns,
+		Name:      name,
+	}
+	svc := new(corev1.Service)
+	if err := i.Client.Get(ctx, key, svc); err != nil {
+		return nil, fmt.Errorf("failed to get service %s/%s: %w", ns, name, err)
+	}
+
+	return svc, nil
+}
+
+// expectedService returns the expected Service based on the provided infra.
+func (i *Infra) expectedService(infra *ir.Infra) *corev1.Service {
+	var ports []corev1.ServicePort
+	for _, listener := range infra.Proxy.Listeners {
+		for _, port := range listener.Ports {
+			// Set the target port based on the protocol of the IR port.
+			target := intstr.IntOrString{IntVal: envoyHTTPPort}
+			if port.Protocol == ir.HTTPSProtocolType {
+				target = intstr.IntOrString{IntVal: envoyHTTPSPort}
+			}
+			p := corev1.ServicePort{
+				Name:       port.Name,
+				Protocol:   corev1.ProtocolTCP,
+				Port:       port.Port,
+				TargetPort: target,
+			}
+			ports = append(ports, p)
+		}
+	}
+
+	svc := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+
+			Namespace: i.Namespace,
+			Name:      infra.Proxy.ObjectName(),
+			Labels:    envoyLabels(),
+		},
+		Spec: corev1.ServiceSpec{
+			Type:            corev1.ServiceTypeLoadBalancer,
+			Ports:           ports,
+			Selector:        EnvoyPodSelector().MatchLabels,
+			SessionAffinity: corev1.ServiceAffinityNone,
+			// Preserve the client source IP and avoid a second hop for LoadBalancer.
+			ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeLocal,
+		},
+	}
+
+	return svc
+}
+
+// createService creates a Service in the kube api server based on the provided infra,
+// if it doesn't exist.
+func (i *Infra) createService(ctx context.Context, infra *ir.Infra) (*corev1.Service, error) {
+	expected := i.expectedService(infra)
+	err := i.Client.Create(ctx, expected)
+	if err != nil {
+		if kerrors.IsAlreadyExists(err) {
+			return expected, nil
+		}
+		return nil, fmt.Errorf("failed to create service %s/%s: %w",
+			expected.Namespace, expected.Name, err)
+	}
+
+	return expected, nil
+}
diff --git a/internal/ir/infra.go b/internal/ir/infra.go
index 869fded9b..f82e67948 100644
--- a/internal/ir/infra.go
+++ b/internal/ir/infra.go
@@ -50,10 +50,26 @@ type ProxyListener struct {
 type ListenerPort struct {
 	// Name is the name of the listener port.
 	Name string
+	// Protocol is the protocol that the listener port will listener for.
+	Protocol ProtocolType
 	// Port is the port number to listen on.
 	Port int32
 }
 
+// ProtocolType defines the application protocol accepted by a ListenerPort.
+//
+// Valid values include "HTTP" and "HTTPS".
+type ProtocolType string
+
+const (
+	// HTTPProtocolType accepts cleartext HTTP/1.1 sessions over TCP or HTTP/2
+	// over cleartext.
+	HTTPProtocolType ProtocolType = "HTTP"
+
+	// HTTPSProtocolType accepts HTTP/1.1 or HTTP/2 sessions over TLS.
+	HTTPSProtocolType ProtocolType = "HTTPS"
+)
+
 // NewInfra returns a new Infra with default parameters.
 func NewInfra() *Infra {
 	return &Infra{
@@ -76,7 +92,7 @@ func NewProxyInfra() *ProxyInfra {
 func NewProxyListeners() []ProxyListener {
 	return []ProxyListener{
 		{
-			Ports: []ListenerPort{},
+			Ports: nil,
 		},
 	}
 }
@@ -138,6 +154,10 @@ func ValidateProxyInfra(pInfra *ProxyInfra) error {
 		errs = append(errs, errors.New("image field required"))
 	}
 
+	if len(pInfra.Listeners) > 1 {
+		errs = append(errs, errors.New("no more than 1 listener is supported"))
+	}
+
 	if len(pInfra.Listeners) > 0 {
 		for i := range pInfra.Listeners {
 			listener := pInfra.Listeners[i]


Test patch:
diff --git a/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-allowed-httproute.out.yaml b/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-allowed-httproute.out.yaml
index 986178215..0bad23884 100644
--- a/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-allowed-httproute.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-allowed-httproute.out.yaml
@@ -75,4 +75,5 @@ infraIR:
       - address: ""
         ports:
         - name: envoy-gateway-gateway-1
+          protocol: "HTTP"
           port: 80
diff --git a/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-disallowed-httproute.out.yaml b/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-disallowed-httproute.out.yaml
index 0c2341fbb..913779acc 100644
--- a/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-disallowed-httproute.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-allows-same-namespace-with-disallowed-httproute.out.yaml
@@ -67,4 +67,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-namespaces-selector.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-namespaces-selector.out.yaml
index ec48a1529..2250899b8 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-namespaces-selector.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-namespaces-selector.out.yaml
@@ -67,6 +67,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 80
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-group.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-group.out.yaml
index 94817d3f7..a11770273 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-group.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-group.out.yaml
@@ -65,6 +65,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 80
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind.out.yaml
index 2818a391d..a5e409fde 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind.out.yaml
@@ -65,6 +65,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 80
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml
index 719cd1ea3..e2e5c0d1b 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml
@@ -65,6 +65,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 443
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml
index dae4ae91d..f147c2913 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml
@@ -63,6 +63,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 443
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml
index bf4acf778..b5df22547 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml
@@ -69,7 +69,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 443
-
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml
index 54b62570d..9fd786bcf 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml
@@ -70,6 +70,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 443
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml
index aafdecd59..375d4e45b 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml
@@ -69,6 +69,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 443
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-missing-allowed-namespaces-selector.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-missing-allowed-namespaces-selector.out.yaml
index beaf6e6a7..6131d047c 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-missing-allowed-namespaces-selector.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-missing-allowed-namespaces-selector.out.yaml
@@ -61,6 +61,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 80
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-secret-in-other-namespace-allowed-by-refgrant.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-secret-in-other-namespace-allowed-by-refgrant.out.yaml
index d6f347b7f..25c5096ca 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-secret-in-other-namespace-allowed-by-refgrant.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-secret-in-other-namespace-allowed-by-refgrant.out.yaml
@@ -83,4 +83,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTPS"
             port: 443
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml
index 5939dee4c..603860943 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml
@@ -62,6 +62,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 80
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml
index 35dae0eb8..adb187307 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml
@@ -82,4 +82,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTPS"
             port: 443
diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml
index 35969ba2b..ad71ae980 100644
--- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml
@@ -85,6 +85,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 80
diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml
index b784ebc4e..d20bf7e35 100644
--- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml
@@ -85,6 +85,3 @@ infraIR:
     image: envoyproxy/envoy-dev:latest
     listeners:
       - address: ""
-        ports:
-          - name: envoy-gateway-gateway-1
-            port: 80
diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners.out.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners.out.yaml
index 2ec810915..1a7f01c8d 100644
--- a/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-attaching-to-gateway-with-two-listeners.out.yaml
@@ -108,4 +108,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-gateway.out.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-gateway.out.yaml
index cf8070df3..fa7c6fd92 100644
--- a/internal/gatewayapi/testdata/httproute-attaching-to-gateway.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-attaching-to-gateway.out.yaml
@@ -75,4 +75,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-listener-on-gateway-with-two-listeners.out.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-listener-on-gateway-with-two-listeners.out.yaml
index 252b7bc34..c7693df42 100644
--- a/internal/gatewayapi/testdata/httproute-attaching-to-listener-on-gateway-with-two-listeners.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-attaching-to-listener-on-gateway-with-two-listeners.out.yaml
@@ -102,4 +102,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-attaching-to-listener.out.yaml b/internal/gatewayapi/testdata/httproute-attaching-to-listener.out.yaml
index 252d819b8..2b009bdc7 100644
--- a/internal/gatewayapi/testdata/httproute-attaching-to-listener.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-attaching-to-listener.out.yaml
@@ -77,4 +77,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-no-weights.out.yaml b/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-no-weights.out.yaml
index afa129b20..2273795a9 100644
--- a/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-no-weights.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-no-weights.out.yaml
@@ -85,4 +85,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-weights.out.yaml b/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-weights.out.yaml
index 27821069c..e710cbafc 100644
--- a/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-weights.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-rule-with-multiple-backends-and-weights.out.yaml
@@ -88,4 +88,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml b/internal/gatewayapi/testdata/httproute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml
index d2b8c793e..4ae946246 100644
--- a/internal/gatewayapi/testdata/httproute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml
@@ -77,4 +77,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backendref-in-other-namespace.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backendref-in-other-namespace.out.yaml
index bdcfc59e7..2f00b99f5 100644
--- a/internal/gatewayapi/testdata/httproute-with-invalid-backendref-in-other-namespace.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-invalid-backendref-in-other-namespace.out.yaml
@@ -79,4 +79,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-with-non-matching-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml b/internal/gatewayapi/testdata/httproute-with-non-matching-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml
index 9aa3322a2..5f31149f0 100644
--- a/internal/gatewayapi/testdata/httproute-with-non-matching-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-non-matching-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml
@@ -72,4 +72,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-with-single-rule-with-exact-path-match.out.yaml b/internal/gatewayapi/testdata/httproute-with-single-rule-with-exact-path-match.out.yaml
index e483a7ce3..789f20c53 100644
--- a/internal/gatewayapi/testdata/httproute-with-single-rule-with-exact-path-match.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-single-rule-with-exact-path-match.out.yaml
@@ -76,4 +76,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-with-single-rule-with-path-prefix-and-exact-header-matches.out.yaml b/internal/gatewayapi/testdata/httproute-with-single-rule-with-path-prefix-and-exact-header-matches.out.yaml
index 46a0be3de..ec063bfac 100644
--- a/internal/gatewayapi/testdata/httproute-with-single-rule-with-path-prefix-and-exact-header-matches.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-single-rule-with-path-prefix-and-exact-header-matches.out.yaml
@@ -85,4 +85,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-with-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml b/internal/gatewayapi/testdata/httproute-with-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml
index 50b23b52a..8476df0b9 100644
--- a/internal/gatewayapi/testdata/httproute-with-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-specific-hostname-attaching-to-gateway-with-wildcard-hostname.out.yaml
@@ -83,4 +83,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/gatewayapi/testdata/httproute-with-two-specific-hostnames-attaching-to-gateway-with-wildcard-hostname.out.yaml b/internal/gatewayapi/testdata/httproute-with-two-specific-hostnames-attaching-to-gateway-with-wildcard-hostname.out.yaml
index 1a841d6f8..7c37f12d0 100644
--- a/internal/gatewayapi/testdata/httproute-with-two-specific-hostnames-attaching-to-gateway-with-wildcard-hostname.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-two-specific-hostnames-attaching-to-gateway-with-wildcard-hostname.out.yaml
@@ -94,4 +94,5 @@ infraIR:
       - address: ""
         ports:
           - name: envoy-gateway-gateway-1
+            protocol: "HTTP"
             port: 80
diff --git a/internal/infrastructure/kubernetes/service_test.go b/internal/infrastructure/kubernetes/service_test.go
new file mode 100644
index 000000000..3fbd3b195
--- /dev/null
+++ b/internal/infrastructure/kubernetes/service_test.go
@@ -0,0 +1,74 @@
+package kubernetes
+
+import (
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	"github.com/envoyproxy/gateway/internal/envoygateway"
+	"github.com/envoyproxy/gateway/internal/ir"
+)
+
+func checkServiceHasPort(t *testing.T, svc *corev1.Service, port int32) {
+	t.Helper()
+
+	for _, p := range svc.Spec.Ports {
+		if p.Port == port {
+			return
+		}
+	}
+	t.Errorf("service is missing port %q", port)
+}
+
+func checkServiceHasTargetPort(t *testing.T, svc *corev1.Service, port int32) {
+	t.Helper()
+
+	intStrPort := intstr.IntOrString{IntVal: port}
+	for _, p := range svc.Spec.Ports {
+		if p.TargetPort == intStrPort {
+			return
+		}
+	}
+	t.Errorf("service is missing targetPort %d", port)
+}
+
+func checkServiceHasPortName(t *testing.T, svc *corev1.Service, name string) {
+	t.Helper()
+
+	for _, p := range svc.Spec.Ports {
+		if p.Name == name {
+			return
+		}
+	}
+	t.Errorf("service is missing port name %q", name)
+}
+
+func TestDesiredService(t *testing.T) {
+	cli := fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).WithObjects().Build()
+	kube := NewInfra(cli)
+	infra := ir.NewInfra()
+	infra.Proxy.Listeners[0].Ports = []ir.ListenerPort{
+		{
+			Name:     "gateway-system-gateway-1",
+			Protocol: ir.HTTPProtocolType,
+			Port:     80,
+		},
+		{
+			Name:     "gateway-system-gateway-1",
+			Protocol: ir.HTTPSProtocolType,
+			Port:     443,
+		},
+	}
+	svc := kube.expectedService(infra)
+
+	checkServiceHasPort(t, svc, envoyServiceHTTPPort)
+	checkServiceHasPort(t, svc, envoyServiceHTTPSPort)
+	checkServiceHasTargetPort(t, svc, envoyHTTPPort)
+	checkServiceHasTargetPort(t, svc, envoyHTTPSPort)
+
+	for _, port := range infra.Proxy.Listeners[0].Ports {
+		checkServiceHasPortName(t, svc, port.Name)
+	}
+}