Repository: kubernetes-sigs/gateway-api

Issue:
Move mesh test app into k8s org
              I'd rather have this image owned by k8s/gateway-api infra so we can bundle versions of it with each release of our conformance tests. Can you create an issue to track following up on that? Shorter term can we at least use a pinned version of this image so a change in this image doesn't have an immediate impact on our conformance tests?

_Originally posted by @robscott in https://github.com/kubernetes-sigs/gateway-api/pull/1878#discussion_r1154742350_
            

Discussion:
@keithmattix: /assign @kflynn @keithmattix @keithmattix: /assign @t-eckert@k8s-ci-robot: @keithmattix: GitHub didn't allow me to assign the following users: t-eckert.

Note that only [kubernetes-sigs members](https://github.com/orgs/kubernetes-sigs/people) with read permissions, repo collaborators and people who have commented on this issue/PR can be assigned. Additionally, issues/PRs can only have 10 assignees at the same time.
For more information please see [the contributor guide](https://git.k8s.io/community/contributors/guide/first-contribution.md#issue-assignment-in-github)

<details>

In response to [this](https://github.com/kubernetes-sigs/gateway-api/issues/1895#issuecomment-1522052255):

>/assign @t-eckert


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>@keithmattix: The code for the app server is here: https://github.com/istio/istio/tree/master/pkg/test/echo. A good first step would be to just move this code into a repo under kubernetes-sigs. @keithmattix: /reopen 
The above PRs don't actual change the ownership of the code or build the image@k8s-ci-robot: @keithmattix: Reopened this issue.

<details>

In response to [this](https://github.com/kubernetes-sigs/gateway-api/issues/1895#issuecomment-1540296757):

>/reopen 
>The above PRs don't actual change the ownership of the code or build the image


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>@robscott: @keithmattix I actually did intend to close this one as I was taking the text of the issue very literally:

> I'd rather have this image owned by k8s/gateway-api infra so we can bundle versions of it with each release of our conformance tests. Can you create an issue to track following up on that? Shorter term can we at least use a pinned version of this image so a change in this image doesn't have an immediate impact on our conformance tests?

So at least from my perspective, the goal was to build an image from within Gateway API and push it to a k8s owned registry. We have accomplished both of those things, despite not actually moving the code into this repo. In my opinion duplicating the code here is not really helpful, the main goal was to ensure that we control the image and can publish updates/version however it works best for us.

I know the actual title of the issue seems to suggest some more broader action, but I don't think it's required, so will close this out. If we want to consider moving code over here maybe we should discuss that in a separate issue.

/close@keithmattix: Ah I misunderstood the previous discussion then; thanks for the clarification!@LiorLieberman: Recently came across this with https://github.com/istio/istio/issues/56400. I was not familiar with this dependency. IMO worth either bringing the image/code here and/or documenting how to build a new image if we need changes (in istio repo) 

/reopen@k8s-ci-robot: @LiorLieberman: Reopened this issue.

<details>

In response to [this](https://github.com/kubernetes-sigs/gateway-api/issues/1895#issuecomment-2932099152):

>Recently came across this with https://github.com/istio/istio/issues/56400. I was not familiar with this dependency. IMO worth either bringing the image/code here and/or documenting how to build a new image if we need changes (in istio repo) 
>
>/reopen


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Reopen this issue with `/reopen`
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/close not-planned

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-ci-robot: @k8s-triage-robot: Closing this issue, marking it as "Not Planned".

<details>

In response to [this](https://github.com/kubernetes-sigs/gateway-api/issues/1895#issuecomment-3469981681):

>The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.
>
>This bot triages issues according to the following rules:
>- After 90d of inactivity, `lifecycle/stale` is applied
>- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
>- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed
>
>You can:
>- Reopen this issue with `/reopen`
>- Mark this issue as fresh with `/remove-lifecycle rotten`
>- Offer to help out with [Issue Triage][1]
>
>Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).
>
>/close not-planned
>
>[1]: https://www.kubernetes.dev/docs/guide/issue-triage/


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>

Solution patch:
diff --git a/apis/v1beta1/gateway_types.go b/apis/v1beta1/gateway_types.go
index 4403942e..8f2d34e9 100644
--- a/apis/v1beta1/gateway_types.go
+++ b/apis/v1beta1/gateway_types.go
@@ -536,6 +536,8 @@ const (
 	//
 	// * "Invalid"
 	// * "Pending"
+	// * "NoResources"
+	// * "AddressNotAssigned"
 	//
 	// Possible reasons for this condition to be Unknown are:
 	//
@@ -550,9 +552,20 @@ const (
 	// true.
 	GatewayReasonProgrammed GatewayConditionReason = "Programmed"
 
-	// This reason is used with the "Programmed" condition when the Listener is
+	// This reason is used with the "Programmed" and "Accepted" condition when the Gateway is
 	// syntactically or semantically invalid.
 	GatewayReasonInvalid GatewayConditionReason = "Invalid"
+
+	// This reason is used with the "Programmed" condition when the
+	// Gateway is not scheduled because insufficient infrastructure
+	// resources are available.
+	GatewayReasonNoResources GatewayConditionReason = "NoResources"
+
+	// This reason is used with the "Programmed" condition when none of the requested
+	// addresses have been assigned to the Gateway. This reason can be used to
+	// express a range of circumstances, including (but not limited to) IPAM
+	// address exhaustion, address not yet allocated, or a named address not being found.
+	GatewayReasonAddressNotAssigned GatewayConditionReason = "AddressNotAssigned"
 )
 
 const (
@@ -564,11 +577,14 @@ const (
 	// Possible reasons for this condition to be True are:
 	//
 	// * "Accepted"
+	// * "ListenersNotValid"
 	//
 	// Possible reasons for this condition to be False are:
 	//
+	// * "Invalid"
 	// * "NotReconciled"
-	// * "NoResources"
+	// * "UnsupportedAddress"
+	// * "ListenersNotValid"
 	//
 	// Possible reasons for this condition to be Unknown are:
 	//
@@ -579,76 +595,65 @@ const (
 	// interoperability.
 	GatewayConditionAccepted GatewayConditionType = "Accepted"
 
-	// Deprecated: use "Accepted" instead.
-	GatewayConditionScheduled GatewayConditionType = "Scheduled"
-
 	// This reason is used with the "Accepted" condition when the condition is
 	// True.
 	GatewayReasonAccepted GatewayConditionReason = "Accepted"
 
+	// This reason is used with the "Accepted" condition when one or
+	// more Listeners have an invalid or unsupported configuration
+	// and cannot be configured on the Gateway.
+	// This can be the reason when "Accepted" is "True" or "False", depending on whether
+	// the listener being invalid causes the entire Gateway to not be accepted.
+	GatewayReasonListenersNotValid GatewayConditionReason = "ListenersNotValid"
+
+	// This reason is used with the "Accepted" and "Programmed"
+	// conditions when the status is "Unknown" and no controller has reconciled
+	// the Gateway.
+	GatewayReasonPending GatewayConditionReason = "Pending"
+
+	// This reason is used with the "Accepted" condition when the Gateway could not be configured
+	// because the requested address is not supported. This reason could be used in a number of
+	// instances, including:
+	//
+	// * The address is already in use.
+	// * The type of address is not supported by the implementation.
+	GatewaReasonUnsupportedAddress GatewayConditionReason = "UnsupportedAddress"
+)
+
+const (
+	// Deprecated: use "Accepted" instead.
+	GatewayConditionScheduled GatewayConditionType = "Scheduled"
+
 	// This reason is used with the "Scheduled" condition when the condition is
 	// True.
 	//
 	// Deprecated: use the "Accepted" condition with reason "Accepted" instead.
 	GatewayReasonScheduled GatewayConditionReason = "Scheduled"
 
-	// This reason is used with the "Accepted", "Programmed" and "Ready"
-	// conditions when the status is "Unknown" and no controller has reconciled
-	// the Gateway.
-	GatewayReasonPending GatewayConditionReason = "Pending"
-
 	// Deprecated: Use "Pending" instead.
 	GatewayReasonNotReconciled GatewayConditionReason = "NotReconciled"
-
-	// This reason is used with the "Accepted" condition when the
-	// Gateway is not scheduled because insufficient infrastructure
-	// resources are available.
-	GatewayReasonNoResources GatewayConditionReason = "NoResources"
 )
 
 const (
-	// Ready is an optional Condition that has Extended support. When it's set,
-	// the condition indicates whether the Gateway has been completely configured
-	// and traffic is ready to flow through the data plane immediately.
-	//
-	// If both the "ListenersNotValid" and "ListenersNotReady"
-	// reasons are true, the Gateway controller should prefer the
-	// "ListenersNotValid" reason.
+	// "Ready" is a reserved condition type for future use. It should not be used by implementations.
+	// Note: This condition is not really "deprecated", but rather "reserved"; however, deprecated triggers Go linters
+	// to alert about usage.
 	//
-	// Possible reasons for this condition to be true are:
-	//
-	// * "Ready"
+	// If used in the future, "Ready" will represent the final state where all configuration is confirmed good
+	// _and has completely propagated to the data plane_. That is, it is a _guarantee_ that, as soon as something
+	// sees the Condition as `true`, then connections will be correctly routed _immediately_.
 	//
-	// Possible reasons for this condition to be False are:
-	//
-	// * "ListenersNotValid"
-	// * "ListenersNotReady"
-	// * "AddressNotAssigned"
+	// This is a very strong guarantee, and to date no implementation has satisfied it enough to implement it.
+	// This reservation can be discussed in the future if necessary.
 	//
-	// Controllers may raise this condition with other reasons,
-	// but should prefer to use the reasons listed above to improve
-	// interoperability.
+	// Deprecated: Ready is reserved for future use
 	GatewayConditionReady GatewayConditionType = "Ready"
 
-	// This reason is used with the "Ready" condition when the condition is
-	// true.
+	// Deprecated: Ready is reserved for future use
 	GatewayReasonReady GatewayConditionReason = "Ready"
 
-	// This reason is used with the "Ready" condition when one or
-	// more Listeners have an invalid or unsupported configuration
-	// and cannot be configured on the Gateway.
-	GatewayReasonListenersNotValid GatewayConditionReason = "ListenersNotValid"
-
-	// This reason is used with the "Ready" condition when one or
-	// more Listeners are not ready to serve traffic.
+	// Deprecated: Ready is reserved for future use
 	GatewayReasonListenersNotReady GatewayConditionReason = "ListenersNotReady"
-
-	// This reason is used with the "Ready" condition when none of the requested
-	// addresses have been assigned to the Gateway. This reason can be used to
-	// express a range of circumstances, including (but not limited to) IPAM
-	// address exhaustion, invalid or unsupported address requests, or a named
-	// address not being found.
-	GatewayReasonAddressNotAssigned GatewayConditionReason = "AddressNotAssigned"
 )
 
 // ListenerStatus is the status associated with a Listener.
@@ -745,7 +750,6 @@ const (
 	//
 	// * "PortUnavailable"
 	// * "UnsupportedProtocol"
-	// * "UnsupportedAddress"
 	//
 	// Possible reasons for this condition to be Unknown are:
 	//
@@ -781,14 +785,6 @@ const (
 	// Listener could not be attached to be Gateway because its
 	// protocol type is not supported.
 	ListenerReasonUnsupportedProtocol ListenerConditionReason = "UnsupportedProtocol"
-
-	// This reason is used with the "Accepted" condition when the Listener could
-	// not be attached to the Gateway because the requested address is not
-	// supported. This reason could be used in a number of instances, including:
-	//
-	// * The address is already in use.
-	// * The type of address is not supported by the implementation.
-	ListenerReasonUnsupportedAddress ListenerConditionReason = "UnsupportedAddress"
 )
 
 const (
@@ -870,34 +866,6 @@ const (
 	// This reason is used with the "Programmed" condition when the condition is
 	// true.
 	ListenerReasonProgrammed ListenerConditionReason = "Programmed"
-)
-
-const (
-	// Ready is an optional Condition that has Extended support. When it's set,
-	// the condition indicates whether the Listener has been configured on the
-	// Gateway and traffic is ready to flow through the data plane immediately.
-	//
-	// Possible reasons for this condition to be True are:
-	//
-	// * "Ready"
-	//
-	// Possible reasons for this condition to be False are:
-	//
-	// * "Invalid"
-	// * "Pending"
-	//
-	// Possible reasons for this condition to be Unknown are:
-	//
-	// * "Pending"
-	//
-	// Controllers may raise this condition with other reasons,
-	// but should prefer to use the reasons listed above to improve
-	// interoperability.
-	ListenerConditionReady ListenerConditionType = "Ready"
-
-	// This reason is used with the "Ready" condition when the condition is
-	// true.
-	ListenerReasonReady ListenerConditionReason = "Ready"
 
 	// This reason is used with the "Ready" and "Programmed" conditions when the
 	// Listener is syntactically or semantically invalid.
@@ -908,3 +876,22 @@ const (
 	// online and ready to accept client traffic.
 	ListenerReasonPending ListenerConditionReason = "Pending"
 )
+
+const (
+	// "Ready" is a reserved condition type for future use. It should not be used by implementations.
+	// Note: This condition is not really "deprecated", but rather "reserved"; however, deprecated triggers Go linters
+	// to alert about usage.
+	//
+	// If used in the future, "Ready" will represent the final state where all configuration is confirmed good
+	// _and has completely propagated to the data plane_. That is, it is a _guarantee_ that, as soon as something
+	// sees the Condition as `true`, then connections will be correctly routed _immediately_.
+	//
+	// This is a very strong guarantee, and to date no implementation has satisfied it enough to implement it.
+	// This reservation can be discussed in the future if necessary.
+	//
+	// Deprecated: Ready is reserved for future use
+	ListenerConditionReady ListenerConditionType = "Ready"
+
+	// Deprecated: Ready is reserved for future use
+	ListenerReasonReady ListenerConditionReason = "Ready"
+)
diff --git a/conformance/embed.go b/conformance/embed.go
index 11b8a12a..06765475 100644
--- a/conformance/embed.go
+++ b/conformance/embed.go
@@ -18,5 +18,5 @@ package conformance
 
 import "embed"
 
-//go:embed tests/* base/*
+//go:embed tests/* base/* mesh/*
 var Manifests embed.FS
diff --git a/conformance/mesh/manifests.yaml b/conformance/mesh/manifests.yaml
new file mode 100644
index 00000000..a69589f1
--- /dev/null
+++ b/conformance/mesh/manifests.yaml
@@ -0,0 +1,130 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: gateway-conformance-mesh
+  labels:
+    gateway-conformance: infra
+---
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: echo-v1
+  namespace: gateway-conformance-mesh
+  labels:
+    app: echo
+spec:
+  selector:
+    matchLabels:
+      app: echo
+      version: v1
+  template:
+    metadata:
+      labels:
+        app: echo
+        version: v1
+    spec:
+      containers:
+      - name: echo
+        image: gcr.io/istio-release/app:1.17.1
+        imagePullPolicy: IfNotPresent
+        args:
+        - --tcp=9090
+        - --port=80
+        - --grpc=7070
+        - --port=443
+        - --tls=443
+        - --crt=/cert.crt
+        - --key=/cert.key
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: echo-v1
+  namespace: gateway-conformance-mesh
+spec:
+  selector:
+    app: echo
+    version: v1
+  ports:
+  - name: http
+    port: 80
+  - name: https
+    port: 443
+  - name: tcp
+    port: 9090
+  - name: tcp-sf
+    port: 9091
+  - name: grpc
+    port: 7070
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: echo-v2
+  namespace: gateway-conformance-mesh
+  labels:
+    app: echo
+spec:
+  selector:
+    matchLabels:
+      app: echo
+      version: v2
+  template:
+    metadata:
+      labels:
+        app: echo
+        version: v2
+    spec:
+      containers:
+      - name: echo
+        image: gcr.io/istio-release/app:1.17.1
+        imagePullPolicy: IfNotPresent
+        args:
+        - --tcp=9090
+        - --port=80
+        - --grpc=7070
+        - --port=443
+        - --tls=443
+        - --crt=/cert.crt
+        - --key=/cert.key
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: echo-v2
+  namespace: gateway-conformance-mesh
+spec:
+  selector:
+    app: echo
+    version: v2
+  ports:
+  - name: http
+    port: 80
+  - name: https
+    port: 443
+  - name: tcp
+    port: 9090
+  - name: grpc
+    port: 7070
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: echo
+  namespace: gateway-conformance-mesh
+spec:
+  selector:
+    app: echo
+  ports:
+  - name: http
+    port: 80
+  - name: https
+    port: 443
+  - name: tcp
+    port: 9090
+  - name: tcp-sf
+    port: 9091
+  - name: grpc
+    port: 7070
diff --git a/conformance/utils/echo/parse.go b/conformance/utils/echo/parse.go
new file mode 100644
index 00000000..a175df92
--- /dev/null
+++ b/conformance/utils/echo/parse.go
@@ -0,0 +1,273 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package echo
+
+import (
+	"fmt"
+	"net/http"
+	"regexp"
+	"sort"
+	"strings"
+)
+
+// Field is a list of fields returned in responses from the Echo server.
+type Field string
+
+const (
+	RequestIDField        Field = "X-Request-Id"
+	ServiceVersionField   Field = "ServiceVersion"
+	ServicePortField      Field = "ServicePort"
+	StatusCodeField       Field = "StatusCode"
+	URLField              Field = "URL"
+	ForwarderURLField     Field = "Url"
+	ForwarderMessageField Field = "Echo"
+	ForwarderHeaderField  Field = "Header"
+	HostField             Field = "Host"
+	HostnameField         Field = "Hostname"
+	MethodField           Field = "Method"
+	ProtocolField         Field = "Proto"
+	AlpnField             Field = "Alpn"
+	RequestHeaderField    Field = "RequestHeader"
+	ResponseHeaderField   Field = "ResponseHeader"
+	ClusterField          Field = "Cluster"
+	IPField               Field = "IP" // The Requester’s IP Address.
+	LatencyField          Field = "Latency"
+	ActiveRequestsField   Field = "ActiveRequests"
+	DNSProtocolField      Field = "Protocol"
+	DNSQueryField         Field = "Query"
+	DNSServerField        Field = "DnsServer"
+	CipherField           Field = "Cipher"
+	TLSVersionField       Field = "Version"
+	TLSServerName         Field = "ServerName"
+)
+
+var (
+	requestIDFieldRegex      = regexp.MustCompile("(?i)" + string(RequestIDField) + "=(.*)")
+	serviceVersionFieldRegex = regexp.MustCompile(string(ServiceVersionField) + "=(.*)")
+	servicePortFieldRegex    = regexp.MustCompile(string(ServicePortField) + "=(.*)")
+	statusCodeFieldRegex     = regexp.MustCompile(string(StatusCodeField) + "=(.*)")
+	hostFieldRegex           = regexp.MustCompile(string(HostField) + "=(.*)")
+	hostnameFieldRegex       = regexp.MustCompile(string(HostnameField) + "=(.*)")
+	requestHeaderFieldRegex  = regexp.MustCompile(string(RequestHeaderField) + "=(.*)")
+	responseHeaderFieldRegex = regexp.MustCompile(string(ResponseHeaderField) + "=(.*)")
+	URLFieldRegex            = regexp.MustCompile(string(URLField) + "=(.*)")
+	ClusterFieldRegex        = regexp.MustCompile(string(ClusterField) + "=(.*)")
+	IPFieldRegex             = regexp.MustCompile(string(IPField) + "=(.*)")
+	methodFieldRegex         = regexp.MustCompile(string(MethodField) + "=(.*)")
+	protocolFieldRegex       = regexp.MustCompile(string(ProtocolField) + "=(.*)")
+	alpnFieldRegex           = regexp.MustCompile(string(AlpnField) + "=(.*)")
+)
+
+// Response represents a response to a single echo request.
+type Response struct {
+	// RequestURL is the requested URL. This differs from URL, which is the just the path.
+	// For example, RequestURL=http://foo/bar, URL=/bar
+	RequestURL string
+	// Method used (for HTTP).
+	Method string
+	// Protocol used for the request.
+	Protocol string
+	// Alpn value (for HTTP).
+	Alpn string
+	// RawContent is the original unparsed content for this response
+	RawContent string
+	// ID is a unique identifier of the resource in the response
+	ID string
+	// URL is the url the request is sent to
+	URL string
+	// Version is the version of the resource in the response
+	Version string
+	// Port is the port of the resource in the response
+	Port string
+	// Code is the response code
+	Code string
+	// Host is the host called by the request
+	Host string
+	// Hostname is the host that responded to the request
+	Hostname string
+	// The cluster where the server is deployed.
+	Cluster string
+	// IP is the requester's ip address
+	IP string
+	// rawBody gives a map of all key/values in the body of the response.
+	rawBody         map[string]string
+	RequestHeaders  http.Header
+	ResponseHeaders http.Header
+}
+
+func ParseResponse(output string) Response {
+	out := Response{
+		RawContent:      output,
+		RequestHeaders:  make(http.Header),
+		ResponseHeaders: make(http.Header),
+	}
+
+	match := requestIDFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.ID = match[1]
+	}
+
+	match = methodFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.Method = match[1]
+	}
+
+	match = protocolFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.Protocol = match[1]
+	}
+
+	match = alpnFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.Alpn = match[1]
+	}
+
+	match = serviceVersionFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.Version = match[1]
+	}
+
+	match = servicePortFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.Port = match[1]
+	}
+
+	match = statusCodeFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.Code = match[1]
+	}
+
+	match = hostFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.Host = match[1]
+	}
+
+	match = hostnameFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.Hostname = match[1]
+	}
+
+	match = URLFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.URL = match[1]
+	}
+
+	match = ClusterFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.Cluster = match[1]
+	}
+
+	match = IPFieldRegex.FindStringSubmatch(output)
+	if match != nil {
+		out.IP = match[1]
+	}
+
+	out.rawBody = map[string]string{}
+
+	matches := requestHeaderFieldRegex.FindAllStringSubmatch(output, -1)
+	for _, kv := range matches {
+		sl := strings.SplitN(kv[1], ":", 2)
+		if len(sl) != 2 {
+			continue
+		}
+		out.RequestHeaders.Set(sl[0], sl[1])
+	}
+
+	matches = responseHeaderFieldRegex.FindAllStringSubmatch(output, -1)
+	for _, kv := range matches {
+		sl := strings.SplitN(kv[1], ":", 2)
+		if len(sl) != 2 {
+			continue
+		}
+		out.ResponseHeaders.Set(sl[0], sl[1])
+	}
+
+	for _, l := range strings.Split(output, "\n") {
+		prefixSplit := strings.Split(l, "body] ")
+		if len(prefixSplit) != 2 {
+			continue
+		}
+		kv := strings.SplitN(prefixSplit[1], "=", 2)
+		if len(kv) != 2 {
+			continue
+		}
+		out.rawBody[kv[0]] = kv[1]
+	}
+
+	return out
+}
+
+// HeaderType is a helper enum for retrieving Headers from a Response.
+type HeaderType string
+
+const (
+	RequestHeader  HeaderType = "request"
+	ResponseHeader HeaderType = "response"
+)
+
+// GetHeaders returns the appropriate headers for the given type.
+func (r Response) GetHeaders(hType HeaderType) http.Header {
+	switch hType {
+	case RequestHeader:
+		return r.RequestHeaders
+	case ResponseHeader:
+		return r.ResponseHeaders
+	default:
+		panic("invalid HeaderType enum: " + hType)
+	}
+}
+
+// Body returns the lines of the response body, in order
+func (r Response) Body() []string {
+	type keyValue struct {
+		k, v string
+	}
+	var keyValues []keyValue
+	// rawBody is in random order, so get the order back via sorting.
+	for k, v := range r.rawBody {
+		keyValues = append(keyValues, keyValue{k, v})
+	}
+	sort.Slice(keyValues, func(i, j int) bool {
+		return keyValues[i].k < keyValues[j].k
+	})
+	var resp []string
+	for _, kv := range keyValues {
+		resp = append(resp, kv.v)
+	}
+	return resp
+}
+
+func (r Response) String() string {
+	out := ""
+	out += fmt.Sprintf("RawContent:       %s\n", r.RawContent)
+	out += fmt.Sprintf("ID:               %s\n", r.ID)
+	out += fmt.Sprintf("Method:           %s\n", r.Method)
+	out += fmt.Sprintf("Protocol:         %s\n", r.Protocol)
+	out += fmt.Sprintf("Alpn:             %s\n", r.Alpn)
+	out += fmt.Sprintf("URL:              %s\n", r.URL)
+	out += fmt.Sprintf("Version:          %s\n", r.Version)
+	out += fmt.Sprintf("Port:             %s\n", r.Port)
+	out += fmt.Sprintf("Code:             %s\n", r.Code)
+	out += fmt.Sprintf("Host:             %s\n", r.Host)
+	out += fmt.Sprintf("Hostname:         %s\n", r.Hostname)
+	out += fmt.Sprintf("Cluster:          %s\n", r.Cluster)
+	out += fmt.Sprintf("IP:               %s\n", r.IP)
+	out += fmt.Sprintf("Request Headers:  %v\n", r.RequestHeaders)
+	out += fmt.Sprintf("Response Headers: %v\n", r.ResponseHeaders)
+
+	return out
+}
diff --git a/conformance/utils/echo/pod.go b/conformance/utils/echo/pod.go
new file mode 100644
index 00000000..3accb81b
--- /dev/null
+++ b/conformance/utils/echo/pod.go
@@ -0,0 +1,155 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package echo
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/remotecommand"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+)
+
+// MeshPod represents a connection to a specific pod running in the mesh.
+// This can be used to trigger requests *from* that pod.
+type MeshPod struct {
+	Name      string
+	Namespace string
+	rc        *rest.RESTClient
+	rcfg      *rest.Config
+}
+
+type MeshApplication string
+
+const (
+	MeshAppEchoV1 MeshApplication = "app=echo,version=v1"
+	MeshAppEchoV2 MeshApplication = "app=echo,version=v2"
+)
+
+func (m *MeshPod) SendRequest(t *testing.T, exp http.ExpectedResponse) {
+	r := exp.Request
+	protocol := strings.ToLower(r.Protocol)
+	if protocol == "" {
+		protocol = "http"
+	}
+	args := []string{"client", fmt.Sprintf("%s://%s/%s", protocol, r.Host, r.Path)}
+	if r.Method != "" {
+		args = append(args, "--method="+r.Method)
+	}
+	if !r.UnfollowRedirect {
+		args = append(args, "--follow-redirects")
+	}
+	for k, v := range r.Headers {
+		args = append(args, "-H", fmt.Sprintf("%v: %v", k, v))
+	}
+
+	resp, err := m.request(args)
+	if err != nil {
+		t.Fatalf("Got error: %v", err)
+	}
+	t.Logf("Got resp %v", resp)
+	want := exp.Response
+	if fmt.Sprint(want.StatusCode) != resp.Code {
+		t.Errorf("wanted status code %v, got %v", want.StatusCode, resp.Code)
+	}
+	for _, name := range want.AbsentHeaders {
+		if v := resp.ResponseHeaders.Values(name); len(v) != 0 {
+			t.Errorf("expected no header %q, got %v", name, v)
+		}
+	}
+	for k, v := range want.Headers {
+		if got := resp.RequestHeaders.Get(k); got != v {
+			t.Errorf("expected header %v=%v, got %v", k, v, got)
+		}
+	}
+}
+
+func (m *MeshPod) request(args []string) (Response, error) {
+	container := "echo"
+
+	req := m.rc.Post().
+		Resource("pods").
+		Name(m.Name).
+		Namespace(m.Namespace).
+		SubResource("exec").
+		Param("container", container).
+		VersionedParams(&v1.PodExecOptions{
+			Container: container,
+			Command:   args,
+			Stdin:     false,
+			Stdout:    true,
+			Stderr:    true,
+			TTY:       false,
+		}, scheme.ParameterCodec)
+
+	exec, err := remotecommand.NewSPDYExecutor(m.rcfg, "POST", req.URL())
+	if err != nil {
+		return Response{}, err
+	}
+
+	var stdoutBuf, stderrBuf bytes.Buffer
+	err = exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
+		Stdin:  nil,
+		Stdout: &stdoutBuf,
+		Stderr: &stderrBuf,
+		Tty:    false,
+	})
+	if err != nil {
+		return Response{}, err
+	}
+
+	return ParseResponse(stdoutBuf.String()), nil
+}
+
+func ConnectToApp(t *testing.T, s *suite.ConformanceTestSuite, app MeshApplication) MeshPod {
+	// hardcoded, for now
+	ns := "gateway-conformance-mesh"
+	metaList := &metav1.PartialObjectMetadataList{}
+	metaList.SetGroupVersionKind(schema.GroupVersionKind{
+		Group:   "",
+		Version: "v1",
+		Kind:    "PodList",
+	})
+
+	err := s.Client.List(context.Background(), metaList, client.InNamespace(ns), client.HasLabels(strings.Split(string(app), ",")))
+	if err != nil {
+		t.Fatalf("failed to query pods in app %v", app)
+	}
+	if len(metaList.Items) == 0 {
+		t.Fatalf("no pods found in app %v", app)
+	}
+	podName := metaList.Items[0].Name
+	podNamespace := metaList.Items[0].Namespace
+
+	return MeshPod{
+		Name:      podName,
+		Namespace: podNamespace,
+		rc:        s.RESTClient,
+		rcfg:      s.RestConfig,
+	}
+}
diff --git a/conformance/utils/flags/flags.go b/conformance/utils/flags/flags.go
index 9da7501b..aa478edb 100644
--- a/conformance/utils/flags/flags.go
+++ b/conformance/utils/flags/flags.go
@@ -31,3 +31,9 @@ var (
 	ExemptFeatures             = flag.String("exempt-features", "", "Exempt Features excluded from conformance tests suites")
 	EnableAllSupportedFeatures = flag.Bool("all-features", false, "Whether to enable all supported features for conformance tests")
 )
+
+// Mesh specific flags
+var (
+	MeshOnly               = flag.Bool("mesh-only", false, "Whether to run mesh conformance tests only")
+	MeshImplementationName = flag.String("mesh-implementation-name", "", "Name of the mesh implementation being tested")
+)
diff --git a/conformance/utils/http/http.go b/conformance/utils/http/http.go
index 93f72dab..35b0485b 100644
--- a/conformance/utils/http/http.go
+++ b/conformance/utils/http/http.go
@@ -64,6 +64,7 @@ type Request struct {
 	Path             string
 	Headers          map[string]string
 	UnfollowRedirect bool
+	Protocol         string
 }
 
 // ExpectedRequest defines expected properties of a request that reaches a backend.
diff --git a/conformance/utils/suite/features.go b/conformance/utils/suite/features.go
index 0a11c53e..9c7a6c85 100644
--- a/conformance/utils/suite/features.go
+++ b/conformance/utils/suite/features.go
@@ -35,6 +35,9 @@ const (
 	// Opting out of this requires an implementation to have clearly implemented
 	// and documented equivalent safeguards.
 	SupportReferenceGrant SupportedFeature = "ReferenceGrant"
+	// This option indicates support for Gateway (core conformance).
+	// Opting out of this is allowed only for GAMMA-only implementations
+	SupportGateway SupportedFeature = "Gateway"
 )
 
 // StandardCoreFeatures are the features that are required to be conformant with
@@ -44,6 +47,7 @@ const (
 // See: https://github.com/kubernetes-sigs/gateway-api/issues/1891
 var StandardCoreFeatures = sets.New(
 	SupportReferenceGrant,
+	SupportGateway,
 )
 
 // -----------------------------------------------------------------------------
@@ -164,6 +168,20 @@ var TLSCoreFeatures = sets.New(
 	SupportTLSRoute,
 )
 
+// -----------------------------------------------------------------------------
+// Features - Mesh Conformance
+// -----------------------------------------------------------------------------
+const (
+	// This option indicates general support for GAMMA
+	SupportGAMMA SupportedFeature = "GAMMA"
+)
+
+// GAMMACoreFeatures includes all the supported features for the GAMMA API at
+// a Core level of support.
+var GAMMACoreFeatures = sets.New(
+	SupportGAMMA,
+)
+
 // -----------------------------------------------------------------------------
 // Features - Compilations
 // -----------------------------------------------------------------------------
@@ -176,4 +194,5 @@ var AllFeatures = sets.New[SupportedFeature]().
 	Insert(StandardExtendedFeatures.UnsortedList()...).
 	Insert(ExperimentalExtendedFeatures.UnsortedList()...).
 	Insert(HTTPExtendedFeatures.UnsortedList()...).
-	Insert(TLSCoreFeatures.UnsortedList()...)
+	Insert(TLSCoreFeatures.UnsortedList()...).
+	Insert(GAMMACoreFeatures.UnsortedList()...)
diff --git a/conformance/utils/suite/suite.go b/conformance/utils/suite/suite.go
index 69d0e795..882c4bd8 100644
--- a/conformance/utils/suite/suite.go
+++ b/conformance/utils/suite/suite.go
@@ -20,6 +20,7 @@ import (
 	"testing"
 
 	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"sigs.k8s.io/gateway-api/apis/v1beta1"
@@ -32,12 +33,15 @@ import (
 // conformance tests.
 type ConformanceTestSuite struct {
 	Client            client.Client
+	RESTClient        *rest.RESTClient
+	RestConfig        *rest.Config
 	RoundTripper      roundtripper.RoundTripper
 	GatewayClassName  string
 	ControllerName    string
 	Debug             bool
 	Cleanup           bool
 	BaseManifests     string
+	MeshManifests     string
 	Applier           kubernetes.Applier
 	SupportedFeatures sets.Set[SupportedFeature]
 	TimeoutConfig     config.TimeoutConfig
@@ -47,10 +51,13 @@ type ConformanceTestSuite struct {
 // Options can be used to initialize a ConformanceTestSuite.
 type Options struct {
 	Client           client.Client
+	RESTClient       *rest.RESTClient
+	RestConfig       *rest.Config
 	GatewayClassName string
 	Debug            bool
 	RoundTripper     roundtripper.RoundTripper
 	BaseManifests    string
+	MeshManifests    string
 	NamespaceLabels  map[string]string
 	// ValidUniqueListenerPorts maps each listener port of each Gateway in the
 	// manifests to a valid, unique port. There must be as many
@@ -92,11 +99,14 @@ func New(s Options) *ConformanceTestSuite {
 
 	suite := &ConformanceTestSuite{
 		Client:           s.Client,
+		RESTClient:       s.RESTClient,
+		RestConfig:       s.RestConfig,
 		RoundTripper:     roundTripper,
 		GatewayClassName: s.GatewayClassName,
 		Debug:            s.Debug,
 		Cleanup:          s.CleanupBaseResources,
 		BaseManifests:    s.BaseManifests,
+		MeshManifests:    s.MeshManifests,
 		Applier: kubernetes.Applier{
 			NamespaceLabels:          s.NamespaceLabels,
 			ValidUniqueListenerPorts: s.ValidUniqueListenerPorts,
@@ -110,6 +120,9 @@ func New(s Options) *ConformanceTestSuite {
 	if suite.BaseManifests == "" {
 		suite.BaseManifests = "base/manifests.yaml"
 	}
+	if suite.MeshManifests == "" {
+		suite.MeshManifests = "mesh/manifests.yaml"
+	}
 
 	return suite
 }
@@ -117,30 +130,43 @@ func New(s Options) *ConformanceTestSuite {
 // Setup ensures the base resources required for conformance tests are installed
 // in the cluster. It also ensures that all relevant resources are ready.
 func (suite *ConformanceTestSuite) Setup(t *testing.T) {
-	t.Logf("Test Setup: Ensuring GatewayClass has been accepted")
-	suite.ControllerName = kubernetes.GWCMustHaveAcceptedConditionTrue(t, suite.Client, suite.TimeoutConfig, suite.GatewayClassName)
-
-	suite.Applier.GatewayClass = suite.GatewayClassName
-	suite.Applier.ControllerName = suite.ControllerName
-
-	t.Logf("Test Setup: Applying base manifests")
-	suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, suite.BaseManifests, suite.Cleanup)
-
-	t.Logf("Test Setup: Applying programmatic resources")
-	secret := kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-web-backend", "certificate", []string{"*"})
-	suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
-	secret = kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-infra", "tls-validity-checks-certificate", []string{"*"})
-	suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
-	secret = kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-infra", "tls-passthrough-checks-certificate", []string{"abc.example.com"})
-	suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
-
-	t.Logf("Test Setup: Ensuring Gateways and Pods from base manifests are ready")
-	namespaces := []string{
-		"gateway-conformance-infra",
-		"gateway-conformance-app-backend",
-		"gateway-conformance-web-backend",
+	if suite.SupportedFeatures.Has(SupportGateway) {
+		t.Logf("Test Setup: Ensuring GatewayClass has been accepted")
+		suite.ControllerName = kubernetes.GWCMustHaveAcceptedConditionTrue(t, suite.Client, suite.TimeoutConfig, suite.GatewayClassName)
+
+		suite.Applier.GatewayClass = suite.GatewayClassName
+		suite.Applier.ControllerName = suite.ControllerName
+
+		t.Logf("Test Setup: Applying base manifests")
+		suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, suite.BaseManifests, suite.Cleanup)
+
+		t.Logf("Test Setup: Applying programmatic resources")
+		secret := kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-web-backend", "certificate", []string{"*"})
+		suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
+		secret = kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-infra", "tls-validity-checks-certificate", []string{"*"})
+		suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
+		secret = kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-infra", "tls-passthrough-checks-certificate", []string{"abc.example.com"})
+		suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
+
+		t.Logf("Test Setup: Ensuring Gateways and Pods from base manifests are ready")
+		namespaces := []string{
+			"gateway-conformance-infra",
+			"gateway-conformance-app-backend",
+			"gateway-conformance-web-backend",
+		}
+		kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, namespaces)
+	}
+	if suite.SupportedFeatures.Has(SupportGAMMA) {
+		t.Logf("Test Setup: Applying base manifests")
+		suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, suite.MeshManifests, suite.Cleanup)
+		t.Logf("Test Setup: Ensuring Gateways and Pods from base manifests are ready")
+		namespaces := []string{
+			"gateway-conformance-mesh",
+			"gateway-conformance-app-backend",
+			"gateway-conformance-web-backend",
+		}
+		kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, namespaces)
 	}
-	kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, namespaces)
 }
 
 // Run runs the provided set of conformance tests.
diff --git a/go.mod b/go.mod
index a9dbe96b..69b51075 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
 	k8s.io/code-generator v0.26.3
 	k8s.io/klog/v2 v2.90.1
 	k8s.io/utils v0.0.0-20221128185143-99ec85e7a448
-	sigs.k8s.io/controller-runtime v0.14.5
+	sigs.k8s.io/controller-runtime v0.14.6
 	sigs.k8s.io/controller-tools v0.11.3
 	sigs.k8s.io/yaml v1.3.0
 )
@@ -41,6 +41,7 @@ require (
 	github.com/mailru/easyjson v0.7.6 // indirect
 	github.com/mattn/go-colorable v0.1.9 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/moby/spdystream v0.2.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
diff --git a/go.sum b/go.sum
index 08993ab3..37f8e8c2 100644
--- a/go.sum
+++ b/go.sum
@@ -35,6 +35,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/ahmetb/gen-crd-api-reference-docs v0.3.0 h1:+XfOU14S4bGuwyvCijJwhhBIjYN+YXS18jrCY2EzJaY=
 github.com/ahmetb/gen-crd-api-reference-docs v0.3.0/go.mod h1:TdjdkYhlOifCQWPs1UdTma97kQQMozf5h26hTuG70u8=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
@@ -49,6 +50,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
 github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
 github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -139,6 +141,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
 github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -173,6 +176,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
 github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM=
+github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
+github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -540,8 +545,8 @@ k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-sigs.k8s.io/controller-runtime v0.14.5 h1:6xaWFqzT5KuAQ9ufgUaj1G/+C4Y1GRkhrxl+BJ9i+5s=
-sigs.k8s.io/controller-runtime v0.14.5/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0=
+sigs.k8s.io/controller-runtime v0.14.6 h1:oxstGVvXGNnMvY7TAESYk+lzr6S3V5VFxQ6d92KcwQA=
+sigs.k8s.io/controller-runtime v0.14.6/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0=
 sigs.k8s.io/controller-tools v0.11.3 h1:T1xzLkog9saiyQSLz1XOImu4OcbdXWytc5cmYsBeBiE=
 sigs.k8s.io/controller-tools v0.11.3/go.mod h1:qcfX7jfcfYD/b7lAhvqAyTbt/px4GpvN88WKLFFv7p8=
 sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
diff --git a/requirements.txt b/requirements.txt
index f299d01c..f78ac785 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,7 +10,7 @@ MarkupSafe==2.1.2
 mkdocs==1.4.2
 mkdocs-awesome-pages-plugin==2.8.0
 mkdocs-macros-plugin==0.7.0
-mkdocs-material==9.1.4
+mkdocs-material==9.1.5
 mkdocs-material-extensions==1.1.1
 mkdocs-redirects==1.2.0
 mkdocs-mermaid2-plugin==0.6.0



Test patch:
diff --git a/conformance/conformance_test.go b/conformance/conformance_test.go
index 803a16f2..777b4402 100644
--- a/conformance/conformance_test.go
+++ b/conformance/conformance_test.go
@@ -63,7 +63,18 @@ func TestConformance(t *testing.T) {
 		EnableAllSupportedFeatures: *flags.EnableAllSupportedFeatures,
 	})
 	cSuite.Setup(t)
-	cSuite.Run(t, tests.ConformanceTests)
+
+	// TODO(https://github.com/kubernetes-sigs/gateway-api/issues/1891) this will likely changed;
+	// for now, we use this mechanism to split mesh and gateway types
+	var toRun []suite.ConformanceTest
+	if supportedFeatures.Has(suite.SupportGateway) {
+		toRun = append(toRun, tests.ConformanceTests...)
+	}
+	if supportedFeatures.Has(suite.SupportGAMMA) {
+		toRun = append(toRun, tests.MeshConformanceTests...)
+	}
+
+	cSuite.Run(t, toRun)
 }
 
 // parseSupportedFeatures parses flag arguments and converts the string to
diff --git a/conformance/tests/main.go b/conformance/tests/main.go
index 7cbcebc2..8a204e45 100644
--- a/conformance/tests/main.go
+++ b/conformance/tests/main.go
@@ -19,3 +19,4 @@ package tests
 import "sigs.k8s.io/gateway-api/conformance/utils/suite"
 
 var ConformanceTests []suite.ConformanceTest
+var MeshConformanceTests []suite.ConformanceTest
diff --git a/conformance/tests/mesh-basic.go b/conformance/tests/mesh-basic.go
new file mode 100644
index 00000000..f08209a0
--- /dev/null
+++ b/conformance/tests/mesh-basic.go
@@ -0,0 +1,55 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tests
+
+import (
+	"testing"
+
+	"sigs.k8s.io/gateway-api/conformance/utils/echo"
+	"sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+)
+
+func init() {
+	MeshConformanceTests = append(MeshConformanceTests, MeshBasic)
+}
+
+var MeshBasic = suite.ConformanceTest{
+	ShortName:   "MeshBasic",
+	Description: "A mesh client can communicate with a mesh server. This tests basic reachability with no configuration applied.",
+	Manifests:   []string{},
+	Test: func(t *testing.T, s *suite.ConformanceTestSuite) {
+		client := echo.ConnectToApp(t, s, echo.MeshAppEchoV1)
+		cases := []http.ExpectedResponse{{
+			Request: http.Request{
+				Host:   "echo",
+				Method: "GET",
+			},
+			Response: http.Response{
+				StatusCode: 200,
+			},
+		}}
+		for i := range cases {
+			// Declare tc here to avoid loop variable
+			// reuse issues across parallel tests.
+			tc := cases[i]
+			t.Run(tc.GetTestCaseName(i), func(t *testing.T) {
+				client.SendRequest(t, tc)
+			})
+		}
+	},
+}
diff --git a/conformance/tests/mesh-split.go b/conformance/tests/mesh-split.go
new file mode 100644
index 00000000..0d4e1619
--- /dev/null
+++ b/conformance/tests/mesh-split.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tests
+
+import (
+	"testing"
+
+	"sigs.k8s.io/gateway-api/conformance/utils/echo"
+	"sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+)
+
+func init() {
+	MeshConformanceTests = append(MeshConformanceTests, MeshTrafficSplit)
+}
+
+var MeshTrafficSplit = suite.ConformanceTest{
+	ShortName:   "MeshTrafficSplit",
+	Description: "A mesh client can send traffic to a Service which is split between two versions",
+	Manifests:   []string{"tests/mesh-split.yaml"},
+	Test: func(t *testing.T, s *suite.ConformanceTestSuite) {
+		client := echo.ConnectToApp(t, s, echo.MeshAppEchoV1)
+		cases := []http.ExpectedResponse{
+			{
+				Request: http.Request{
+					Host:   "echo",
+					Method: "GET",
+					Path:   "/v1",
+				},
+				Response: http.Response{
+					StatusCode: 200,
+				},
+				Backend: "echo-v1",
+			},
+			{
+				Request: http.Request{
+					Host:   "echo",
+					Method: "GET",
+					Path:   "/v2",
+				},
+				Response: http.Response{
+					StatusCode: 200,
+				},
+				Backend: "echo-v2",
+			},
+		}
+		for i := range cases {
+			// Declare tc here to avoid loop variable
+			// reuse issues across parallel tests.
+			tc := cases[i]
+			t.Run(tc.GetTestCaseName(i), func(t *testing.T) {
+				client.SendRequest(t, tc)
+			})
+		}
+	},
+}
diff --git a/conformance/tests/mesh-split.yaml b/conformance/tests/mesh-split.yaml
new file mode 100644
index 00000000..293eef9f
--- /dev/null
+++ b/conformance/tests/mesh-split.yaml
@@ -0,0 +1,24 @@
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: HTTPRoute
+metadata:
+  name: mesh-split
+  namespace: gateway-conformance-mesh
+spec:
+  parentRefs:
+  - kind: Service
+    name: echo
+  rules:
+  - matches:
+    - path:
+        type: Exact
+        value: /v1
+    backendRefs:
+    - name: echo-v1
+      port: 80
+  - matches:
+    - path:
+        type: Exact
+        value: /v2
+    backendRefs:
+    - name: echo-v2
+      port: 80