Skip to content

Commit 593f8d9

Browse files
authored
feat: support http2 and h2c app protocol (#10687)
1 parent 7cb91cd commit 593f8d9

File tree

12 files changed

+248
-59
lines changed

12 files changed

+248
-59
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1243,7 +1243,7 @@ $(TEST_ASSET_DIR)/conformance/conformance_test.go:
12431243
cat $(shell go list -json -m sigs.k8s.io/gateway-api | jq -r '.Dir')/conformance/conformance_test.go >> $@
12441244
go fmt $@
12451245

1246-
CONFORMANCE_SUPPORTED_FEATURES ?= -supported-features=Gateway,ReferenceGrant,HTTPRoute,HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRouteResponseHeaderModification,HTTPRoutePortRedirect,HTTPRouteHostRewrite,HTTPRouteSchemeRedirect,HTTPRoutePathRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,HTTPRouteRequestMirror,TLSRoute
1246+
CONFORMANCE_SUPPORTED_FEATURES ?= -supported-features=Gateway,ReferenceGrant,HTTPRoute,HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRouteResponseHeaderModification,HTTPRoutePortRedirect,HTTPRouteHostRewrite,HTTPRouteSchemeRedirect,HTTPRoutePathRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,HTTPRouteRequestMirror,TLSRoute,HTTPRouteBackendProtocolH2C
12471247
CONFORMANCE_SUPPORTED_PROFILES ?= -conformance-profiles=GATEWAY-HTTP
12481248
CONFORMANCE_REPORT_ARGS ?= -report-output=$(TEST_ASSET_DIR)/conformance/$(VERSION)-report.yaml -organization=solo.io -project=gloo-gateway -version=$(VERSION) -url=github.com/solo-io/gloo -contact=github.com/solo-io/gloo/issues/new/choose
12491249
CONFORMANCE_ARGS := -gateway-class=gloo-gateway $(CONFORMANCE_SUPPORTED_FEATURES) $(CONFORMANCE_SUPPORTED_PROFILES) $(CONFORMANCE_REPORT_ARGS)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
changelog:
2+
- type: NEW_FEATURE
3+
issueLink: https://github.com/solo-io/solo-projects/issues/7824
4+
resolvesIssue: false
5+
description: Adds support for http2 via the service port appProtocol spec
6+

projects/gloo/pkg/plugins/kubernetes/serviceconverter/use_http2_annotation_converter.go

+13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ var http2PortNames = []string{
1919
"http2",
2020
}
2121

22+
var http2AppProtocolNames = map[string]bool{
23+
// Defined by istio : https://istio.io/latest/docs/ops/configuration/traffic-management/protocol-selection/
24+
"http2": true,
25+
"grpc": true,
26+
"grpc-web": true,
27+
// Defined by GEP-1911 : https://gateway-api.sigs.k8s.io/geps/gep-1911/#api-semantics
28+
"kubernetes.io/h2c": true,
29+
}
30+
2231
// UseHttp2Converter sets UseHttp2 on the upstream if:
2332
// (1) the service has the "h2_service" annotation; or
2433
// (2) the "h2_service" annotation defined in Settings.UpstreamOptions; or
@@ -46,6 +55,10 @@ func useHttp2(ctx context.Context, svc *corev1.Service, port corev1.ServicePort)
4655
}
4756
}
4857

58+
if port.AppProtocol != nil && http2AppProtocolNames[*port.AppProtocol] {
59+
return &wrappers.BoolValue{Value: true}
60+
}
61+
4962
for _, http2Name := range http2PortNames {
5063
if strings.HasPrefix(port.Name, http2Name) {
5164
return &wrappers.BoolValue{Value: true}

projects/gloo/pkg/plugins/kubernetes/uds_convert_test.go

+23
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,29 @@ var _ = Describe("UdsConvert", func() {
101101
Entry("exactly http2", "http2"),
102102
)
103103

104+
DescribeTable("should create upstream with use_http2=true when port appProtocol is a supported type", func(appProtocol string, useHttp2 bool) {
105+
svc := &corev1.Service{
106+
Spec: corev1.ServiceSpec{},
107+
}
108+
svc.Name = "test"
109+
svc.Namespace = "test-ns"
110+
111+
port := corev1.ServicePort{
112+
Port: 123,
113+
AppProtocol: &appProtocol,
114+
}
115+
up := uc.CreateUpstream(context.TODO(), svc, port)
116+
Expect(up.GetUseHttp2().GetValue()).To(Equal(useHttp2))
117+
},
118+
Entry("http2", "http2", true),
119+
Entry("grpc", "grpc", true),
120+
Entry("grpc-web", "grpc-web", true),
121+
Entry("kubernetes.io/h2c", "kubernetes.io/h2c", true),
122+
Entry("http2-suffix", "http2-suffix", false),
123+
Entry("grpc-suffix", "grpc-suffix", false),
124+
Entry("tcp", "tcp", false),
125+
)
126+
104127
Describe("Upstream Config when Annotations Exist", func() {
105128

106129
It("Should create upstream with use_http2=true when annotation exists", testSetUseHttp2Converter)

test/gomega/matchers/have_http_response.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ type HttpResponse struct {
7474
// Each header can be of type: {string, GomegaMatcher}
7575
// Optional: If not provided, does not perform header validation
7676
Headers map[string]interface{}
77+
// Protocol is the expected protocol of an http.Response
78+
// Optional: If not provided, does not perform additional validation
79+
Protocol string
7780
// Custom is a generic matcher that can be applied to validate any other properties of an http.Response
7881
// Optional: If not provided, does not perform additional validation
7982
Custom types.GomegaMatcher
@@ -90,8 +93,8 @@ func (r *HttpResponse) String() string {
9093
bodyString = fmt.Sprintf("%#v", bodyMatcher)
9194
}
9295

93-
return fmt.Sprintf("HttpResponse{StatusCode: %d, Body: %s, Headers: %v, Custom: %v}",
94-
r.StatusCode, bodyString, r.Headers, r.Custom)
96+
return fmt.Sprintf("HttpResponse{StatusCode: %d, Body: %s, Headers: %v, Protocol: %s, Custom: %v}",
97+
r.StatusCode, bodyString, r.Headers, r.Protocol, r.Custom)
9598

9699
}
97100

@@ -116,6 +119,9 @@ func HaveHttpResponse(expected *HttpResponse) types.GomegaMatcher {
116119
Expected: expected.Body,
117120
})
118121
}
122+
if expected.Protocol != "" {
123+
partialResponseMatchers = append(partialResponseMatchers, HaveProtocol(expected.Protocol))
124+
}
119125
for headerName, headerMatch := range expected.Headers {
120126
partialResponseMatchers = append(partialResponseMatchers, &matchers.HaveHTTPHeaderWithValueMatcher{
121127
Header: headerName,

test/gomega/matchers/have_protocol.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package matchers
2+
3+
import (
4+
"github.com/onsi/gomega"
5+
"github.com/onsi/gomega/gstruct"
6+
"github.com/onsi/gomega/types"
7+
"github.com/solo-io/gloo/test/gomega/transforms"
8+
)
9+
10+
const (
11+
HTTP1Protocol = "HTTP/1.1"
12+
HTTP2Protocol = "HTTP/2"
13+
)
14+
15+
// HaveProtocol expects an http response with the given protocol
16+
func HaveProtocol(protocol string) types.GomegaMatcher {
17+
if protocol == "" {
18+
// If protocol is not defined, we create a matcher that always succeeds
19+
return gstruct.Ignore()
20+
}
21+
//nolint:bodyclose // The caller of this matcher constructor should be responsible for ensuring the body close
22+
return gomega.WithTransform(transforms.WithProtocol(), gomega.Equal(protocol))
23+
}
24+
25+
// HaveHTTP1Protocol expects an http response with the HTTP/1.1 protocol
26+
func HaveHTTP1Protocol() types.GomegaMatcher {
27+
return HaveProtocol(HTTP1Protocol)
28+
}
29+
30+
// HaveHTTP2Protocol expects an http response with the HTTP/2 protocol
31+
func HaveHTTP2Protocol() types.GomegaMatcher {
32+
return HaveProtocol(HTTP2Protocol)
33+
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package matchers_test
2+
3+
import (
4+
"net/http"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
"github.com/solo-io/gloo/test/gomega/matchers"
9+
)
10+
11+
var _ = Describe("HaveProtocol", func() {
12+
13+
It("succeeds if there is no protocol specified", func() {
14+
httpResponse := &http.Response{
15+
Proto: matchers.HTTP1Protocol,
16+
}
17+
Expect(httpResponse).To(matchers.HaveProtocol(""))
18+
})
19+
20+
It("matches the specified protocol", func() {
21+
httpResponse := &http.Response{
22+
Proto: matchers.HTTP1Protocol,
23+
}
24+
Expect(httpResponse).To(matchers.HaveProtocol(matchers.HTTP1Protocol))
25+
Expect(httpResponse).To(matchers.HaveHTTP1Protocol())
26+
Expect(httpResponse).ToNot(matchers.HaveHTTP2Protocol())
27+
})
28+
})

test/gomega/transforms/curl.go

+16-10
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ const (
2222
// WithCurlHttpResponse is a Gomega Transform that converts the string returned by an exec.Curl
2323
// and transforms it into an http.Response. This is useful to be used in tandem with matchers.HaveHttpResponse
2424
// NOTE: This is not feature complete, as we do not convert the entire response.
25-
// For now, we handle HTTP/1.1 response headers, status, and body.
25+
// For now, we handle HTTP/1.1 && HTTP/2 response headers, status, protocol and body.
2626
// The curl must be executed with verbose=true to include both the response headers/status
2727
// and response body.
2828
func WithCurlHttpResponse(curlResponse string) *http.Response {
2929
headers := make(http.Header)
3030
statusCode := 0
31+
protocol := ""
3132
var bodyBuf bytes.Buffer
3233

3334
for _, line := range strings.Split(curlResponse, "\n") {
@@ -37,9 +38,10 @@ func WithCurlHttpResponse(curlResponse string) *http.Response {
3738
continue
3839
}
3940

40-
code := processResponseCode(line)
41+
proto, code := processResponseCodeAndProtocol(line)
4142
if code != 0 {
4243
statusCode = code
44+
protocol = proto
4345
continue
4446
}
4547

@@ -52,6 +54,7 @@ func WithCurlHttpResponse(curlResponse string) *http.Response {
5254
}
5355

5456
return &http.Response{
57+
Proto: protocol,
5558
StatusCode: statusCode,
5659
Header: headers,
5760
Body: bytesBody(bodyBuf.Bytes()),
@@ -61,6 +64,7 @@ func WithCurlHttpResponse(curlResponse string) *http.Response {
6164
func WithCurlResponse(curlResponse *kubectl.CurlResponse) *http.Response {
6265
headers := make(http.Header)
6366
statusCode := 0
67+
protocol := ""
6468
var bodyBuf bytes.Buffer
6569

6670
// Curl writes the body to stdout and the headers/status to stderr
@@ -72,16 +76,18 @@ func WithCurlResponse(curlResponse *kubectl.CurlResponse) *http.Response {
7276
continue
7377
}
7478

75-
code := processResponseCode(line)
79+
proto, code := processResponseCodeAndProtocol(line)
7680
if code != 0 {
7781
statusCode = code
82+
protocol = proto
7883
}
7984
}
8085

8186
// Body
8287
bodyBuf.WriteString(curlResponse.StdOut)
8388

8489
return &http.Response{
90+
Proto: protocol,
8591
StatusCode: statusCode,
8692
Header: headers,
8793
Body: bytesBody(bodyBuf.Bytes()),
@@ -102,21 +108,21 @@ func processResponseHeader(line string) (string, string) {
102108
return "", ""
103109
}
104110

105-
// processResponseCode processes the current line if it's a response status code.
106-
// Returns the status code if the line was processed, otherwise returns 0.
107-
func processResponseCode(line string) int {
111+
// processResponseCodeAndProtocol processes the current line if it's a response status code with the protocol.
112+
// Returns the status code and protocol if the line was processed, otherwise returns 0 and an empty string.
113+
func processResponseCodeAndProtocol(line string) (string, int) {
108114
// check for response status. the line with the response code will be in the format
109-
// `< HTTP/1.1 <code> <message>` or `< HTTP/2 <code> <message>`
115+
// `< HTTP/1.1 <code> <message>` or `< HTTP/2 <code>`
110116
if strings.HasPrefix(line, responseStatusPrefix1dot1) || strings.HasPrefix(line, responseStatusPrefix2) {
111117
statusParts := strings.Split(line, " ")
112-
if len(statusParts) >= 4 {
118+
if len(statusParts) >= 3 {
113119
statusCode, err := strconv.Atoi(statusParts[2])
114120
if err == nil {
115-
return statusCode
121+
return statusParts[1], statusCode
116122
}
117123
}
118124
}
119-
return 0
125+
return "", 0
120126
}
121127

122128
// isResponseBody returns true if the current line is part of the response body, false otherwise.

test/gomega/transforms/transforms.go

+7
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,10 @@ func BytesToInt(b []byte) int {
6767
Expect(err).NotTo(HaveOccurred())
6868
return i
6969
}
70+
71+
// WithProtocol returns a Gomega Transform that extracts the protocol of a request
72+
func WithProtocol() func(response *http.Response) string {
73+
return func(response *http.Response) string {
74+
return response.Proto
75+
}
76+
}

test/kubernetes/e2e/features/services/httproute/suite.go

+22-45
Original file line numberDiff line numberDiff line change
@@ -10,45 +10,23 @@ import (
1010
"github.com/solo-io/gloo/projects/gateway2/wellknown"
1111
"github.com/solo-io/gloo/test/kubernetes/e2e"
1212
"github.com/solo-io/gloo/test/kubernetes/e2e/defaults"
13+
"github.com/solo-io/gloo/test/kubernetes/e2e/tests/base"
1314
)
1415

1516
// testingSuite is the entire Suite of tests for testing K8s Service-specific features/fixes
1617
type testingSuite struct {
17-
suite.Suite
18-
19-
ctx context.Context
20-
21-
// testInstallation contains all the metadata/utilities necessary to execute a series of tests
22-
// against an installation of Gloo Gateway
23-
testInstallation *e2e.TestInstallation
18+
*base.BaseTestingSuite
2419
}
2520

2621
func NewTestingSuite(ctx context.Context, testInst *e2e.TestInstallation) suite.TestingSuite {
2722
return &testingSuite{
28-
ctx: ctx,
29-
testInstallation: testInst,
23+
base.NewBaseTestingSuite(ctx, testInst, base.SimpleTestCase{}, testCases),
3024
}
3125
}
3226

3327
func (s *testingSuite) TestConfigureHTTPRouteBackingDestinationsWithService() {
34-
s.T().Cleanup(func() {
35-
err := s.testInstallation.Actions.Kubectl().DeleteFile(s.ctx, routeWithServiceManifest)
36-
s.NoError(err, "can delete manifest")
37-
err = s.testInstallation.Actions.Kubectl().DeleteFile(s.ctx, serviceManifest)
38-
s.NoError(err, "can delete manifest")
39-
s.testInstallation.Assertions.EventuallyObjectsNotExist(s.ctx, proxyService, proxyDeployment)
40-
})
41-
42-
err := s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, routeWithServiceManifest)
43-
s.Assert().NoError(err, "can apply gloo.solo.io Route manifest")
44-
45-
// apply the service manifest separately, after the route table is applied, to ensure it can be applied after the route table
46-
err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, serviceManifest)
47-
s.Assert().NoError(err, "can apply gloo.solo.io Service manifest")
48-
49-
s.testInstallation.Assertions.EventuallyObjectsExist(s.ctx, proxyService, proxyDeployment)
50-
s.testInstallation.Assertions.AssertEventualCurlResponse(
51-
s.ctx,
28+
s.TestInstallation.Assertions.AssertEventualCurlResponse(
29+
s.Ctx,
5230
defaults.CurlPodExecOpt,
5331
[]curl.Option{
5432
curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)),
@@ -59,34 +37,33 @@ func (s *testingSuite) TestConfigureHTTPRouteBackingDestinationsWithService() {
5937

6038
func (s *testingSuite) TestConfigureHTTPRouteBackingDestinationsWithServiceAndWithoutTCPRoute() {
6139
s.T().Cleanup(func() {
62-
err := s.testInstallation.Actions.Kubectl().DeleteFile(s.ctx, routeWithServiceManifest)
63-
s.NoError(err, "can delete manifest")
64-
err = s.testInstallation.Actions.Kubectl().DeleteFile(s.ctx, serviceManifest)
65-
s.NoError(err, "can delete manifest")
66-
s.testInstallation.Assertions.EventuallyObjectsNotExist(s.ctx, proxyService, proxyDeployment)
67-
err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, tcpRouteCrdManifest)
40+
err := s.TestInstallation.Actions.Kubectl().ApplyFile(s.Ctx, tcpRouteCrdManifest)
6841
s.NoError(err, "can apply manifest")
69-
s.testInstallation.Assertions.EventuallyObjectsExist(s.ctx, &wellknown.TCPRouteCRD)
42+
s.TestInstallation.Assertions.EventuallyObjectsExist(s.Ctx, &wellknown.TCPRouteCRD)
7043
})
7144

7245
// Remove the TCPRoute CRD to assert HTTPRoute services still work.
73-
err := s.testInstallation.Actions.Kubectl().DeleteFile(s.ctx, tcpRouteCrdManifest)
46+
err := s.TestInstallation.Actions.Kubectl().DeleteFile(s.Ctx, tcpRouteCrdManifest)
7447
s.NoError(err, "can delete manifest")
7548

76-
err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, routeWithServiceManifest)
77-
s.Assert().NoError(err, "can apply gloo.solo.io Route manifest")
78-
79-
// apply the service manifest separately, after the route table is applied, to ensure it can be applied after the route table
80-
err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, serviceManifest)
81-
s.Assert().NoError(err, "can apply gloo.solo.io Service manifest")
82-
83-
s.testInstallation.Assertions.EventuallyObjectsExist(s.ctx, proxyService, proxyDeployment)
84-
s.testInstallation.Assertions.AssertEventualCurlResponse(
85-
s.ctx,
49+
s.TestInstallation.Assertions.AssertEventualCurlResponse(
50+
s.Ctx,
8651
defaults.CurlPodExecOpt,
8752
[]curl.Option{
8853
curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)),
8954
curl.WithHostHeader("example.com"),
9055
},
9156
expectedSvcResp)
9257
}
58+
59+
func (s *testingSuite) TestHTTP2AppProtocol() {
60+
s.TestInstallation.Assertions.AssertEventualCurlResponse(
61+
s.Ctx,
62+
defaults.CurlPodExecOpt,
63+
[]curl.Option{
64+
curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)),
65+
curl.WithHostHeader("example.com"),
66+
curl.WithArgs([]string{"--http2-prior-knowledge"}),
67+
},
68+
expectedHTTP2SvcResp)
69+
}

0 commit comments

Comments
 (0)