diff --git a/conformance/base/manifests.yaml b/conformance/base/manifests.yaml index e486973284..f43be72547 100644 --- a/conformance/base/manifests.yaml +++ b/conformance/base/manifests.yaml @@ -507,3 +507,150 @@ spec: resources: requests: cpu: 10m +--- +apiVersion: v1 +kind: Service +metadata: + name: grpc-infra-backend-v1 + namespace: gateway-conformance-infra +spec: + selector: + app: grpc-infra-backend-v1 + ports: + - protocol: TCP + port: 8080 + targetPort: 3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grpc-infra-backend-v1 + namespace: gateway-conformance-infra + labels: + app: grpc-infra-backend-v1 +spec: + replicas: 2 + selector: + matchLabels: + app: grpc-infra-backend-v1 + template: + metadata: + labels: + app: grpc-infra-backend-v1 + spec: + containers: + - name: grpc-infra-backend-v1 + # TODO: Migrate to gcr.io/k8s-staging-gateway-api once possible + image: us-docker.pkg.dev/grpc-testing/testing-images-public/gateway-api/echo-basic:alpha1 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: GRPC_ECHO_SERVER + value: "1" + resources: + requests: + cpu: 10m +--- +apiVersion: v1 +kind: Service +metadata: + name: grpc-infra-backend-v2 + namespace: gateway-conformance-infra +spec: + selector: + app: grpc-infra-backend-v2 + ports: + - protocol: TCP + port: 8080 + targetPort: 3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grpc-infra-backend-v2 + namespace: gateway-conformance-infra + labels: + app: grpc-infra-backend-v2 +spec: + replicas: 2 + selector: + matchLabels: + app: grpc-infra-backend-v2 + template: + metadata: + labels: + app: grpc-infra-backend-v2 + spec: + containers: + - name: grpc-infra-backend-v2 + # TODO: Migrate to gcr.io/k8s-staging-gateway-api once possible + image: us-docker.pkg.dev/grpc-testing/testing-images-public/gateway-api/echo-basic:alpha1 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: GRPC_ECHO_SERVER + value: "1" + resources: + requests: + cpu: 10m +--- +apiVersion: v1 +kind: Service +metadata: + name: grpc-infra-backend-v3 + namespace: gateway-conformance-infra +spec: + selector: + app: grpc-infra-backend-v3 + ports: + - protocol: TCP + port: 8080 + targetPort: 3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grpc-infra-backend-v3 + namespace: gateway-conformance-infra + labels: + app: grpc-infra-backend-v3 +spec: + replicas: 2 + selector: + matchLabels: + app: grpc-infra-backend-v3 + template: + metadata: + labels: + app: grpc-infra-backend-v3 + spec: + containers: + - name: grpc-infra-backend-v3 + # TODO: Migrate to gcr.io/k8s-staging-gateway-api once possible + image: us-docker.pkg.dev/grpc-testing/testing-images-public/gateway-api/echo-basic:alpha1 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: GRPC_ECHO_SERVER + value: "1" + resources: + requests: + cpu: 10m diff --git a/conformance/echo-basic/.gitignore b/conformance/echo-basic/.gitignore new file mode 100644 index 0000000000..3a8c509e75 --- /dev/null +++ b/conformance/echo-basic/.gitignore @@ -0,0 +1 @@ +echo-basic diff --git a/conformance/echo-basic/.go.mod b/conformance/echo-basic/.go.mod new file mode 100644 index 0000000000..eb32d3c281 --- /dev/null +++ b/conformance/echo-basic/.go.mod @@ -0,0 +1,16 @@ +module sigs.k8s.io/gateway-api/conformance/echo-basic + +go 1.21 + +require ( + golang.org/x/net v0.21.0 + google.golang.org/grpc v1.53.0 + google.golang.org/protobuf v1.28.1 +) + +require ( + github.com/golang/protobuf v1.5.2 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect +) diff --git a/conformance/echo-basic/.go.sum b/conformance/echo-basic/.go.sum new file mode 100644 index 0000000000..f5b0dd4ce3 --- /dev/null +++ b/conformance/echo-basic/.go.sum @@ -0,0 +1,21 @@ +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/conformance/echo-basic/echo-basic.go b/conformance/echo-basic/echo-basic.go index 465e303167..98f8de5a7a 100644 --- a/conformance/echo-basic/echo-basic.go +++ b/conformance/echo-basic/echo-basic.go @@ -33,6 +33,8 @@ import ( "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "golang.org/x/net/websocket" + + g "sigs.k8s.io/gateway-api/conformance/echo-basic/grpc" ) // RequestAssertions contains information about the request and the Ingress @@ -62,7 +64,7 @@ type preserveSlashes struct { } func (s *preserveSlashes) ServeHTTP(w http.ResponseWriter, r *http.Request) { - r.URL.Path = strings.Replace(r.URL.Path, "//", "/", -1) + r.URL.Path = strings.ReplaceAll(r.URL.Path, "//", "/") s.mux.ServeHTTP(w, r) } @@ -77,6 +79,11 @@ type Context struct { var context Context func main() { + if os.Getenv("GRPC_ECHO_SERVER") != "" { + g.Main() + return + } + httpPort := os.Getenv("HTTP_PORT") if httpPort == "" { httpPort = "3000" @@ -109,7 +116,7 @@ func main() { go func() { fmt.Printf("Starting server, listening on port %s (http)\n", httpPort) - err := http.ListenAndServe(fmt.Sprintf(":%s", httpPort), httpHandler) + err := http.ListenAndServe(fmt.Sprintf(":%s", httpPort), httpHandler) //nolint:gosec if err != nil { errchan <- err } @@ -137,12 +144,12 @@ func wsHandler(ws *websocket.Conn) { fmt.Println("established websocket connection", ws.RemoteAddr()) // Echo websocket frames from the connection back to the client // until io.EOF - io.Copy(ws, ws) + _, _ = io.Copy(ws, ws) } -func healthHandler(w http.ResponseWriter, r *http.Request) { +func healthHandler(w http.ResponseWriter, r *http.Request) { //nolint:revive w.WriteHeader(200) - w.Write([]byte(`OK`)) + _, _ = w.Write([]byte(`OK`)) } func statusHandler(w http.ResponseWriter, r *http.Request) { @@ -224,7 +231,7 @@ func echoHandler(w http.ResponseWriter, r *http.Request) { writeEchoResponseHeaders(w, r.Header) w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Content-Type-Options", "nosniff") - w.Write(js) + _, _ = w.Write(js) } func writeEchoResponseHeaders(w http.ResponseWriter, headers http.Header) { @@ -242,7 +249,7 @@ func writeEchoResponseHeaders(w http.ResponseWriter, headers http.Header) { } } -func processError(w http.ResponseWriter, err error, code int) { +func processError(w http.ResponseWriter, err error, code int) { //nolint:unparam w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Content-Type-Options", "nosniff") body, err := json.Marshal(struct { @@ -257,7 +264,7 @@ func processError(w http.ResponseWriter, err error, code int) { } w.WriteHeader(code) - w.Write(body) + _, _ = w.Write(body) } func listenAndServeTLS(addr string, serverCert string, serverPrivKey string, clientCA string, handler http.Handler) error { @@ -280,7 +287,7 @@ func listenAndServeTLS(addr string, serverCert string, serverPrivKey string, cli config.ClientCAs = certPool } - srv := &http.Server{ + srv := &http.Server{ //nolint:gosec Addr: addr, Handler: handler, TLSConfig: &config, @@ -311,11 +318,15 @@ func tlsStateToAssertions(connectionState *tls.ConnectionState) *TLSAssertions { // Convert peer certificates to PEM blocks. for _, c := range connectionState.PeerCertificates { var out strings.Builder - pem.Encode(&out, &pem.Block{ + err := pem.Encode(&out, &pem.Block{ Type: "CERTIFICATE", Bytes: c.Raw, }) - state.PeerCertificates = append(state.PeerCertificates, out.String()) + if err != nil { + fmt.Printf("failed to encode certificate: %v\n", err) + } else { + state.PeerCertificates = append(state.PeerCertificates, out.String()) + } } return &state diff --git a/conformance/echo-basic/echo-basic_test.go b/conformance/echo-basic/echo-basic_test.go index 4b37a90f5b..5cbe4b31d6 100644 --- a/conformance/echo-basic/echo-basic_test.go +++ b/conformance/echo-basic/echo-basic_test.go @@ -29,7 +29,7 @@ import ( func TestHealthHandler(t *testing.T) { // Test a valid health check - req, err := http.NewRequest("GET", "/health", nil) + req, err := http.NewRequest("GET", "/health", nil) //nolint:noctx if err != nil { t.Fatal(err) } @@ -50,7 +50,7 @@ func TestHealthHandler(t *testing.T) { func TestDelayResponse(t *testing.T) { // Test with a valid delay integer value - req, err := http.NewRequest("GET", "/?delay=1s", nil) + req, err := http.NewRequest("GET", "/?delay=1s", nil) //nolint:noctx if err != nil { t.Fatal(err) } @@ -61,7 +61,7 @@ func TestDelayResponse(t *testing.T) { } // Test with a valid delay decimal value - req, err = http.NewRequest("GET", "/?delay=0.1s", nil) + req, err = http.NewRequest("GET", "/?delay=0.1s", nil) //nolint:noctx if err != nil { t.Fatal(err) } @@ -72,7 +72,7 @@ func TestDelayResponse(t *testing.T) { } // Test with an invalid delay value - req, err = http.NewRequest("GET", "/?delay=invalid", nil) + req, err = http.NewRequest("GET", "/?delay=invalid", nil) //nolint:noctx if err != nil { t.Fatal(err) } diff --git a/conformance/echo-basic/go.mod b/conformance/echo-basic/go.mod deleted file mode 100644 index d60eb3906c..0000000000 --- a/conformance/echo-basic/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module sigs.k8s.io/gateway-api/conformance/echo-basic - -go 1.21 - -require golang.org/x/net v0.21.0 - -require golang.org/x/text v0.14.0 // indirect diff --git a/conformance/echo-basic/go.sum b/conformance/echo-basic/go.sum deleted file mode 100644 index 98280c8927..0000000000 --- a/conformance/echo-basic/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/conformance/echo-basic/grpc/grpc.go b/conformance/echo-basic/grpc/grpc.go new file mode 100644 index 0000000000..25d467973b --- /dev/null +++ b/conformance/echo-basic/grpc/grpc.go @@ -0,0 +1,268 @@ +/* +Copyright 2024 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 grpc + +import ( + "context" + "crypto/tls" + "encoding/pem" + "fmt" + "net" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/reflection" + + pb "sigs.k8s.io/gateway-api/conformance/echo-basic/grpcechoserver" +) + +type serverConfig struct { + // Controlled by HTTP_PORT env var + HTTPPort int + + // Controlled by multiple env vars -- one for each field: + // - NAMESPACE + // - INGRESS_NAME + // - SERVICE_NAME + // - POD_NAME + PodContext *pb.Context + + // Controlled by TLS_SERVER_CERT env var + TLSServerCert string + + // Controlled by TLS_SERVER_PRIVKEY env var + TLSServerPrivKey string + + // Controlled by HTPPS_PORT env var + HTTPSPort int +} + +type echoServer struct { + pb.UnimplementedGrpcEchoServer + fullService string + tls bool + podContext *pb.Context +} + +func fullMethod(svc, method string) string { + return fmt.Sprintf("/%s/%s", svc, method) +} + +func (s *echoServer) fullMethod(method string) string { + return fullMethod(s.fullService, method) +} + +func (s *echoServer) doEcho(methodName string, ctx context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) { //nolint:revive // Method signature is determined by gRPC. + connectionType := "plaintext" + if s.tls { + connectionType = "TLS" + } + fmt.Printf("Received over %s: %v\n", connectionType, in) + mdElems, ok := metadata.FromIncomingContext(ctx) + if !ok { + msg := "Failed to retrieve metadata from incoming request.\n" + fmt.Print(msg) + return nil, fmt.Errorf(msg) + } + authority := "" + headers := []*pb.Header{} + for k, vs := range mdElems { + for _, v := range vs { + if k == ":authority" { + authority = v + } + headers = append(headers, &pb.Header{ + Key: k, + Value: v, + }) + } + } + resp := &pb.EchoResponse{ + Assertions: &pb.Assertions{ + FullyQualifiedMethod: s.fullMethod(methodName), + Headers: headers, + Authority: authority, + Context: s.podContext, + }, + } + if s.tls { + // TODO: Pull this out into a function so that we can unit test it. + tlsAssertions := &pb.TLSAssertions{} + p, ok := peer.FromContext(ctx) + if !ok { + msg := "Failed to retrieve auth info from request\n" + fmt.Print(msg) + return nil, fmt.Errorf(msg) + } + tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo) + if !ok { + msg := "Failed to retrieve TLS info from request\n" + fmt.Print(msg) + return nil, fmt.Errorf(msg) + } + switch tlsInfo.State.Version { + case tls.VersionTLS13: + tlsAssertions.Version = "TLSv1.3" + case tls.VersionTLS12: + tlsAssertions.Version = "TLSv1.2" + case tls.VersionTLS11: + tlsAssertions.Version = "TLSv1.1" + case tls.VersionTLS10: + tlsAssertions.Version = "TLSv1.0" + } + + tlsAssertions.NegotiatedProtocol = tlsInfo.State.NegotiatedProtocol + tlsAssertions.ServerName = tlsInfo.State.ServerName + tlsAssertions.CipherSuite = tls.CipherSuiteName(tlsInfo.State.CipherSuite) + + // Convert peer certificates to PEM blocks. + for _, c := range tlsInfo.State.PeerCertificates { + var out strings.Builder + err := pem.Encode(&out, &pem.Block{ + Type: "CERTIFICATE", + Bytes: c.Raw, + }) + if err != nil { + fmt.Printf("failed to encode certificate: %v\n", err) + } else { + tlsAssertions.PeerCertificates = append(tlsAssertions.PeerCertificates, out.String()) + } + } + resp.Assertions.TlsAssertions = tlsAssertions + } + return resp, nil +} + +func (s *echoServer) Echo(ctx context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) { + return s.doEcho("Echo", ctx, in) +} + +func (s *echoServer) EchoTwo(ctx context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) { + return s.doEcho("EchoTwo", ctx, in) +} + +func runServer(config serverConfig) (int, int) { //nolint:unparam + svcs := pb.File_grpcecho_proto.Services() + svcd := svcs.ByName("GrpcEcho") + if svcd == nil { + fmt.Println("failed to look up service GrpcEcho.") + os.Exit(1) + } + fullService := string(svcd.FullName()) + + // Set up plaintext server. + lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", config.HTTPPort)) + if err != nil { + fmt.Printf("failed to listen: %v\n", err) + os.Exit(1) + } + resolvedHTTPPort := lis.Addr().(*net.TCPAddr).Port + s := grpc.NewServer() + pb.RegisterGrpcEchoServer(s, &echoServer{fullService: fullService, tls: false, podContext: config.PodContext}) + reflection.Register(s) + + fmt.Printf("plaintext server listening at %v\n", lis.Addr()) + + go func() { + if err := s.Serve(lis); err != nil { + fmt.Printf("failed to serve: %v\n", err) + os.Exit(1) + } + }() + + resolvedHTTPSPort := -1 + if config.TLSServerCert != "" && config.TLSServerPrivKey != "" { + // Set up TLS server. + creds, err := credentials.NewServerTLSFromFile(config.TLSServerCert, config.TLSServerPrivKey) + if err != nil { + fmt.Printf("failed to create credentials: %v\n", err) + os.Exit(1) + } + secureListener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", config.HTTPSPort)) + if err != nil { + fmt.Printf("failed to listen: %v\n", err) + os.Exit(1) + } + resolvedHTTPSPort = secureListener.Addr().(*net.TCPAddr).Port + secureServer := grpc.NewServer(grpc.Creds(creds)) + pb.RegisterGrpcEchoServer(secureServer, &echoServer{fullService: fullService, tls: true, podContext: config.PodContext}) + reflection.Register(secureServer) + + fmt.Printf("secure server listening at %v\n", secureListener.Addr()) + go func() { + err := secureServer.Serve(secureListener) + if err != nil { + fmt.Printf("failed to serve: %v\n", err) + os.Exit(1) + } + }() + } + + return resolvedHTTPPort, resolvedHTTPSPort +} + +func Main() { + podContext := &pb.Context{ + Namespace: os.Getenv("NAMESPACE"), + Ingress: os.Getenv("INGRESS_NAME"), + ServiceName: os.Getenv("SERVICE_NAME"), + Pod: os.Getenv("POD_NAME"), + } + var err error + httpPortStr := os.Getenv("HTTP_PORT") + var httpPort int + if httpPortStr == "" { + httpPort = 3000 + } else { + httpPort, err = strconv.Atoi(httpPortStr) + if err != nil { + fmt.Printf("non-integer value in HTTP_PORT '%s': %v\n", httpPortStr, err) + os.Exit(1) + } + } + + httpsPortStr := os.Getenv("HTTPS_PORT") + var httpsPort int + if httpsPortStr == "" { + httpsPort = 8443 + } else { + httpsPort, err = strconv.Atoi(httpsPortStr) + if err != nil { + fmt.Printf("non-integer value in HTTPS_PORT '%s': %v\n", httpsPortStr, err) + os.Exit(1) + } + } + + config := serverConfig{ + HTTPPort: httpPort, + PodContext: podContext, + TLSServerCert: os.Getenv("TLS_SERVER_CERT"), + TLSServerPrivKey: os.Getenv("TLS_SERVER_PRIV_KEY"), + HTTPSPort: httpsPort, + } + runServer(config) + done := make(chan os.Signal, 1) + signal.Notify(done, syscall.SIGINT, syscall.SIGTERM) + <-done +} diff --git a/conformance/echo-basic/grpc/grpcechoserver_test.go b/conformance/echo-basic/grpc/grpcechoserver_test.go new file mode 100644 index 0000000000..3cf7b9cfcc --- /dev/null +++ b/conformance/echo-basic/grpc/grpcechoserver_test.go @@ -0,0 +1,175 @@ +/* +Copyright 2024 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 grpc + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + + pb "sigs.k8s.io/gateway-api/conformance/echo-basic/grpcechoserver" +) + +const ServerAddress = "127.0.0.1" + +// Let the kernel resolve an open port so multiple test instances can run concurrently. +const ( + ServerHTTPPort = 0 + ServerHTTPSPort = 0 + RPCTimeout = 10 * time.Second +) + +const letters = "abcdefghijklmnopqrstuvwxyz0123456789" + +func randStr(length int) string { //nolint:unparam + s := "" + for i := 0; i < length; i++ { + letter := letters[rand.Int()%len(letters)] //nolint:gosec // This is test code. + s += string([]byte{letter}) + } + return s +} + +type methodFunc = func(context.Context, pb.GrpcEchoClient, *pb.EchoRequest) (*pb.EchoResponse, error) + +func clientAndServer(t *testing.T) (pb.GrpcEchoClient, serverConfig, string) { + t.Helper() + podContext := &pb.Context{ + Namespace: randStr(12), + Ingress: randStr(12), + ServiceName: randStr(12), + Pod: randStr(12), + } + config := serverConfig{ + HTTPPort: ServerHTTPPort, + PodContext: podContext, + } + httpPort, _ := runServer(config) + + dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} + + serverTarget := fmt.Sprintf("%s:%d", ServerAddress, httpPort) + conn, err := grpc.Dial(serverTarget, dialOpts...) + if err != nil { + t.Fatal(err) + } + + stub := pb.NewGrpcEchoClient(conn) + return stub, config, serverTarget +} + +func testEchoMethod(t *testing.T, methodName string, f methodFunc) { + t.Helper() + stub, config, serverTarget := clientAndServer(t) + + const testHeaderKey = "foo" + testHeaderValue := randStr(12) + ctx, cancel := context.WithTimeout(context.Background(), RPCTimeout) + defer cancel() + ctx = metadata.AppendToOutgoingContext(ctx, testHeaderKey, testHeaderValue) + + req := pb.EchoRequest{} + resp, err := f(ctx, stub, &req) + if err != nil { + t.Fatal(err) + } + + echoedReq := pb.EchoRequest{} + + // nil is equivalent to default value. + if resp.GetRequest() != nil { + echoedReq = *resp.GetRequest() + } + + if !proto.Equal(&echoedReq, &req) { + t.Fatalf("echoed request did not equal sent request. expected: %s\ngot: %s\n", prototext.Format(&req), prototext.Format(&echoedReq)) + } + + if resp.GetAssertions() == nil { + t.Fatalf("no assertions populated in response: %v", resp.GetAssertions()) + } + + const fullyQualifiedService = "/gateway_api_conformance.echo_basic.grpcecho.GrpcEcho/" + expectedFullyQualifiedMethod := fullyQualifiedService + methodName + if resp.GetAssertions().GetFullyQualifiedMethod() != expectedFullyQualifiedMethod { + t.Fatalf("fully_qualified_method wrong. expected: %s, got: %s", resp.GetAssertions().GetFullyQualifiedMethod(), expectedFullyQualifiedMethod) + } + + if resp.GetAssertions().GetAuthority() != serverTarget { + t.Fatalf("serverTarget wrong. expected: %s, got: %s", resp.GetAssertions().GetAuthority(), serverTarget) + } + + if resp.GetAssertions().GetContext() == nil || !proto.Equal(resp.GetAssertions().GetContext(), config.PodContext) { + t.Fatalf("podContext wrong. expected %s\ngot: %s", prototext.Format(config.PodContext), prototext.Format(resp.GetAssertions().GetContext())) + } + + echoedTestHeaderValues := []string{} + for _, header := range resp.GetAssertions().GetHeaders() { + if header.GetKey() == testHeaderKey { + echoedTestHeaderValues = append(echoedTestHeaderValues, header.GetValue()) + } + } + + if len(echoedTestHeaderValues) != 1 { + t.Fatalf("echoed header value had unexpected size %d: %v", len(echoedTestHeaderValues), echoedTestHeaderValues) + } + + echoedTestHeaderValue := echoedTestHeaderValues[0] + + if echoedTestHeaderValue != testHeaderValue { + t.Fatalf("echoed header value was wrong. expected: %s, got: %s", testHeaderValue, echoedTestHeaderValue) + } +} + +func TestEchoMethod(t *testing.T) { + testEchoMethod(t, "Echo", func(ctx context.Context, stub pb.GrpcEchoClient, req *pb.EchoRequest) (*pb.EchoResponse, error) { + return stub.Echo(ctx, req) + }) +} + +func TestEchoTwoMethod(t *testing.T) { + testEchoMethod(t, "EchoTwo", func(ctx context.Context, stub pb.GrpcEchoClient, req *pb.EchoRequest) (*pb.EchoResponse, error) { + return stub.EchoTwo(ctx, req) + }) +} + +func TestEchoThreeMethod(t *testing.T) { + stub, _, _ := clientAndServer(t) + ctx, cancel := context.WithTimeout(context.Background(), RPCTimeout) + defer cancel() + req := pb.EchoRequest{} + resp, err := stub.EchoThree(ctx, &req) + if err == nil { + t.Fatalf("Expected RPC to fail but got success: %v", resp) + } + + code := status.Code(err) + if code != codes.Unimplemented { + t.Fatalf("Expected code Unimplemented but found %v: %v", code, err) + } +} diff --git a/conformance/echo-basic/grpcecho.proto b/conformance/echo-basic/grpcecho.proto new file mode 100644 index 0000000000..86c3fe6aed --- /dev/null +++ b/conformance/echo-basic/grpcecho.proto @@ -0,0 +1,96 @@ +// Copyright 2024 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. + +syntax = "proto3"; + +option go_package = "sigs.k8s.io/gateway-api/conformance/echo-basic/grpcechoserver"; + +// This package name should in general be kept in sync with the directory in which it lives. +package gateway_api_conformance.echo_basic.grpcecho; + +message Header { + string key = 1; + string value = 2; +} + +message Context { + // The Kubernetes namespace in which this server is running. Populated by the + // NAMESPACE environment variable. + string namespace = 1; + + // The name of the ingress controller under test. Populated by the INGRESS_NAME + // environment variable. + string ingress = 2; + + // The name service cannot be used here since it is a reserved word. Populated by the + // SERVICE_NAME environment variable. + string service_name = 3; + + // The name of the pod in which this server is running. Populated by the POD_NAME + // environment variable. + string pod = 4; +} + +message TLSAssertions { + // The TLS version used by the connection, e.g. "TLSv1.3" + string version = 1; + + // The negotatiated protocol. + string negotiated_protocol = 2; + + // The server name indication extension sent by the client. + string server_name = 3; + + // The cipher suite negotatiated for the connection, e.g. "TLS_EDCHE_ECDSA_WITH_AES_128_GCM_SHA256" + string cipher_suite = 4; + + // The parsed certificates sent by the peer, in the order in which they were sent. + repeated string peer_certificates = 5; +} + +message Assertions { + // The fully qualified method of the current RPC, e.g. + // "/gateway_api_conformance.echo_basic.grpcecho.GrpcEcho/Echo" + string fully_qualified_method = 1; + + // The headers present in the request. + repeated Header headers = 2; + + // The :authority pseudo-header of the request. + string authority = 3; + + // Information associated with the conformance server deployment. + Context context = 4; + + // Information related to the TLS connection between the client and the server. + TLSAssertions tls_assertions = 5; +} + +message EchoRequest {} + +message EchoResponse { + Assertions assertions = 1; + EchoRequest request = 2; +} + +service GrpcEcho { + rpc Echo(EchoRequest) returns (EchoResponse) {} + + // Behaves identically to Echo, but lives at a different method to + // emulate the service having more than one method. + rpc EchoTwo(EchoRequest) returns (EchoResponse) {} + + // An intentionally unimplemented method. + rpc EchoThree(EchoRequest) returns (EchoResponse) {} +} diff --git a/conformance/echo-basic/grpcechoserver/grpcecho.pb.go b/conformance/echo-basic/grpcechoserver/grpcecho.pb.go new file mode 100644 index 0000000000..9585b7760b --- /dev/null +++ b/conformance/echo-basic/grpcechoserver/grpcecho.pb.go @@ -0,0 +1,670 @@ +// Copyright 2024 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. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v4.22.2 +// source: grpcecho.proto + +// This package name should in general be kept in sync with the directory in which it lives. + +package grpcechoserver + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Header struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *Header) Reset() { + *x = Header{} + if protoimpl.UnsafeEnabled { + mi := &file_grpcecho_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Header) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Header) ProtoMessage() {} + +func (x *Header) ProtoReflect() protoreflect.Message { + mi := &file_grpcecho_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Header.ProtoReflect.Descriptor instead. +func (*Header) Descriptor() ([]byte, []int) { + return file_grpcecho_proto_rawDescGZIP(), []int{0} +} + +func (x *Header) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *Header) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type Context struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The Kubernetes namespace in which this server is running. Populated by the + // NAMESPACE environment variable. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // The name of the ingress controller under test. Populated by the INGRESS_NAME + // environment variable. + Ingress string `protobuf:"bytes,2,opt,name=ingress,proto3" json:"ingress,omitempty"` + // The name service cannot be used here since it is a reserved word. Populated by the + // SERVICE_NAME environment variable. + ServiceName string `protobuf:"bytes,3,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + // The name of the pod in which this server is running. Populated by the POD_NAME + // environment variable. + Pod string `protobuf:"bytes,4,opt,name=pod,proto3" json:"pod,omitempty"` +} + +func (x *Context) Reset() { + *x = Context{} + if protoimpl.UnsafeEnabled { + mi := &file_grpcecho_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Context) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Context) ProtoMessage() {} + +func (x *Context) ProtoReflect() protoreflect.Message { + mi := &file_grpcecho_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Context.ProtoReflect.Descriptor instead. +func (*Context) Descriptor() ([]byte, []int) { + return file_grpcecho_proto_rawDescGZIP(), []int{1} +} + +func (x *Context) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *Context) GetIngress() string { + if x != nil { + return x.Ingress + } + return "" +} + +func (x *Context) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *Context) GetPod() string { + if x != nil { + return x.Pod + } + return "" +} + +type TLSAssertions struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The TLS version used by the connection, e.g. "TLSv1.3" + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + // The negotatiated protocol. + NegotiatedProtocol string `protobuf:"bytes,2,opt,name=negotiated_protocol,json=negotiatedProtocol,proto3" json:"negotiated_protocol,omitempty"` + // The server name indication extension sent by the client. + ServerName string `protobuf:"bytes,3,opt,name=server_name,json=serverName,proto3" json:"server_name,omitempty"` + // The cipher suite negotatiated for the connection, e.g. "TLS_EDCHE_ECDSA_WITH_AES_128_GCM_SHA256" + CipherSuite string `protobuf:"bytes,4,opt,name=cipher_suite,json=cipherSuite,proto3" json:"cipher_suite,omitempty"` + // The parsed certificates sent by the peer, in the order in which they were sent. + PeerCertificates []string `protobuf:"bytes,5,rep,name=peer_certificates,json=peerCertificates,proto3" json:"peer_certificates,omitempty"` +} + +func (x *TLSAssertions) Reset() { + *x = TLSAssertions{} + if protoimpl.UnsafeEnabled { + mi := &file_grpcecho_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TLSAssertions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TLSAssertions) ProtoMessage() {} + +func (x *TLSAssertions) ProtoReflect() protoreflect.Message { + mi := &file_grpcecho_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TLSAssertions.ProtoReflect.Descriptor instead. +func (*TLSAssertions) Descriptor() ([]byte, []int) { + return file_grpcecho_proto_rawDescGZIP(), []int{2} +} + +func (x *TLSAssertions) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *TLSAssertions) GetNegotiatedProtocol() string { + if x != nil { + return x.NegotiatedProtocol + } + return "" +} + +func (x *TLSAssertions) GetServerName() string { + if x != nil { + return x.ServerName + } + return "" +} + +func (x *TLSAssertions) GetCipherSuite() string { + if x != nil { + return x.CipherSuite + } + return "" +} + +func (x *TLSAssertions) GetPeerCertificates() []string { + if x != nil { + return x.PeerCertificates + } + return nil +} + +type Assertions struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The fully qualified method of the current RPC, e.g. + // "/gateway_api_conformance.echo_basic.grpcecho.GrpcEcho/Echo" + FullyQualifiedMethod string `protobuf:"bytes,1,opt,name=fully_qualified_method,json=fullyQualifiedMethod,proto3" json:"fully_qualified_method,omitempty"` + // The headers present in the request. + Headers []*Header `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty"` + // The :authority pseudo-header of the request. + Authority string `protobuf:"bytes,3,opt,name=authority,proto3" json:"authority,omitempty"` + // Information associated with the conformance server deployment. + Context *Context `protobuf:"bytes,4,opt,name=context,proto3" json:"context,omitempty"` + // Information related to the TLS connection between the client and the server. + TlsAssertions *TLSAssertions `protobuf:"bytes,5,opt,name=tls_assertions,json=tlsAssertions,proto3" json:"tls_assertions,omitempty"` +} + +func (x *Assertions) Reset() { + *x = Assertions{} + if protoimpl.UnsafeEnabled { + mi := &file_grpcecho_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Assertions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Assertions) ProtoMessage() {} + +func (x *Assertions) ProtoReflect() protoreflect.Message { + mi := &file_grpcecho_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Assertions.ProtoReflect.Descriptor instead. +func (*Assertions) Descriptor() ([]byte, []int) { + return file_grpcecho_proto_rawDescGZIP(), []int{3} +} + +func (x *Assertions) GetFullyQualifiedMethod() string { + if x != nil { + return x.FullyQualifiedMethod + } + return "" +} + +func (x *Assertions) GetHeaders() []*Header { + if x != nil { + return x.Headers + } + return nil +} + +func (x *Assertions) GetAuthority() string { + if x != nil { + return x.Authority + } + return "" +} + +func (x *Assertions) GetContext() *Context { + if x != nil { + return x.Context + } + return nil +} + +func (x *Assertions) GetTlsAssertions() *TLSAssertions { + if x != nil { + return x.TlsAssertions + } + return nil +} + +type EchoRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *EchoRequest) Reset() { + *x = EchoRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_grpcecho_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EchoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EchoRequest) ProtoMessage() {} + +func (x *EchoRequest) ProtoReflect() protoreflect.Message { + mi := &file_grpcecho_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EchoRequest.ProtoReflect.Descriptor instead. +func (*EchoRequest) Descriptor() ([]byte, []int) { + return file_grpcecho_proto_rawDescGZIP(), []int{4} +} + +type EchoResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Assertions *Assertions `protobuf:"bytes,1,opt,name=assertions,proto3" json:"assertions,omitempty"` + Request *EchoRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` +} + +func (x *EchoResponse) Reset() { + *x = EchoResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_grpcecho_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EchoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EchoResponse) ProtoMessage() {} + +func (x *EchoResponse) ProtoReflect() protoreflect.Message { + mi := &file_grpcecho_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EchoResponse.ProtoReflect.Descriptor instead. +func (*EchoResponse) Descriptor() ([]byte, []int) { + return file_grpcecho_proto_rawDescGZIP(), []int{5} +} + +func (x *EchoResponse) GetAssertions() *Assertions { + if x != nil { + return x.Assertions + } + return nil +} + +func (x *EchoResponse) GetRequest() *EchoRequest { + if x != nil { + return x.Request + } + return nil +} + +var File_grpcecho_proto protoreflect.FileDescriptor + +var file_grpcecho_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x2b, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x61, 0x70, 0x69, 0x5f, 0x63, 0x6f, + 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x62, + 0x61, 0x73, 0x69, 0x63, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x63, 0x68, 0x6f, 0x22, 0x30, 0x0a, + 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, + 0x76, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x6e, 0x67, 0x72, + 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x69, 0x6e, 0x67, 0x72, 0x65, + 0x73, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x6f, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x70, 0x6f, 0x64, 0x22, 0xcb, 0x01, 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x41, + 0x73, 0x73, 0x65, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x2f, 0x0a, 0x13, 0x6e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x12, 0x6e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x5f, + 0x73, 0x75, 0x69, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x69, 0x70, + 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x70, 0x65, 0x65, 0x72, + 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x10, 0x70, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x73, 0x22, 0xe2, 0x02, 0x0a, 0x0a, 0x41, 0x73, 0x73, 0x65, 0x72, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x66, 0x75, 0x6c, 0x6c, 0x79, 0x5f, 0x71, 0x75, + 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x66, 0x75, 0x6c, 0x6c, 0x79, 0x51, 0x75, 0x61, 0x6c, 0x69, + 0x66, 0x69, 0x65, 0x64, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x4d, 0x0a, 0x07, 0x68, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x67, 0x61, + 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x61, 0x70, 0x69, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x62, 0x61, 0x73, 0x69, 0x63, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x4e, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x78, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x5f, 0x61, 0x70, 0x69, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x07, + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x61, 0x0a, 0x0e, 0x74, 0x6c, 0x73, 0x5f, 0x61, + 0x73, 0x73, 0x65, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x3a, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x61, 0x70, 0x69, 0x5f, 0x63, 0x6f, + 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x62, + 0x61, 0x73, 0x69, 0x63, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x54, 0x4c, + 0x53, 0x41, 0x73, 0x73, 0x65, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0d, 0x74, 0x6c, 0x73, + 0x41, 0x73, 0x73, 0x65, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x0d, 0x0a, 0x0b, 0x45, 0x63, + 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbb, 0x01, 0x0a, 0x0c, 0x45, 0x63, + 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0a, 0x61, 0x73, + 0x73, 0x65, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, + 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x61, 0x70, 0x69, 0x5f, 0x63, 0x6f, 0x6e, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x62, 0x61, + 0x73, 0x69, 0x63, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x41, 0x73, 0x73, + 0x65, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x72, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x52, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x61, + 0x70, 0x69, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x65, + 0x63, 0x68, 0x6f, 0x5f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x63, + 0x68, 0x6f, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x07, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x32, 0x91, 0x03, 0x0a, 0x08, 0x47, 0x72, 0x70, 0x63, + 0x45, 0x63, 0x68, 0x6f, 0x12, 0x7d, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x38, 0x2e, 0x67, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x61, 0x70, 0x69, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, + 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x62, 0x61, 0x73, 0x69, + 0x63, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x5f, 0x61, 0x70, 0x69, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x80, 0x01, 0x0a, 0x07, 0x45, 0x63, 0x68, 0x6f, 0x54, 0x77, 0x6f, 0x12, + 0x38, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x61, 0x70, 0x69, 0x5f, 0x63, 0x6f, + 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x62, + 0x61, 0x73, 0x69, 0x63, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x45, 0x63, + 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x67, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x5f, 0x61, 0x70, 0x69, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x6e, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x82, 0x01, 0x0a, 0x09, 0x45, 0x63, 0x68, 0x6f, 0x54, + 0x68, 0x72, 0x65, 0x65, 0x12, 0x38, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x61, + 0x70, 0x69, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x65, + 0x63, 0x68, 0x6f, 0x5f, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x63, + 0x68, 0x6f, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, + 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x61, 0x70, 0x69, 0x5f, 0x63, 0x6f, 0x6e, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x62, 0x61, + 0x73, 0x69, 0x63, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x45, 0x63, 0x68, + 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x3f, 0x5a, 0x3d, 0x73, + 0x69, 0x67, 0x73, 0x2e, 0x6b, 0x38, 0x73, 0x2e, 0x69, 0x6f, 0x2f, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x2f, 0x65, 0x63, 0x68, 0x6f, 0x2d, 0x62, 0x61, 0x73, 0x69, 0x63, 0x2f, 0x67, 0x72, + 0x70, 0x63, 0x65, 0x63, 0x68, 0x6f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_grpcecho_proto_rawDescOnce sync.Once + file_grpcecho_proto_rawDescData = file_grpcecho_proto_rawDesc +) + +func file_grpcecho_proto_rawDescGZIP() []byte { + file_grpcecho_proto_rawDescOnce.Do(func() { + file_grpcecho_proto_rawDescData = protoimpl.X.CompressGZIP(file_grpcecho_proto_rawDescData) + }) + return file_grpcecho_proto_rawDescData +} + +var file_grpcecho_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_grpcecho_proto_goTypes = []interface{}{ + (*Header)(nil), // 0: gateway_api_conformance.echo_basic.grpcecho.Header + (*Context)(nil), // 1: gateway_api_conformance.echo_basic.grpcecho.Context + (*TLSAssertions)(nil), // 2: gateway_api_conformance.echo_basic.grpcecho.TLSAssertions + (*Assertions)(nil), // 3: gateway_api_conformance.echo_basic.grpcecho.Assertions + (*EchoRequest)(nil), // 4: gateway_api_conformance.echo_basic.grpcecho.EchoRequest + (*EchoResponse)(nil), // 5: gateway_api_conformance.echo_basic.grpcecho.EchoResponse +} +var file_grpcecho_proto_depIdxs = []int32{ + 0, // 0: gateway_api_conformance.echo_basic.grpcecho.Assertions.headers:type_name -> gateway_api_conformance.echo_basic.grpcecho.Header + 1, // 1: gateway_api_conformance.echo_basic.grpcecho.Assertions.context:type_name -> gateway_api_conformance.echo_basic.grpcecho.Context + 2, // 2: gateway_api_conformance.echo_basic.grpcecho.Assertions.tls_assertions:type_name -> gateway_api_conformance.echo_basic.grpcecho.TLSAssertions + 3, // 3: gateway_api_conformance.echo_basic.grpcecho.EchoResponse.assertions:type_name -> gateway_api_conformance.echo_basic.grpcecho.Assertions + 4, // 4: gateway_api_conformance.echo_basic.grpcecho.EchoResponse.request:type_name -> gateway_api_conformance.echo_basic.grpcecho.EchoRequest + 4, // 5: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho.Echo:input_type -> gateway_api_conformance.echo_basic.grpcecho.EchoRequest + 4, // 6: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho.EchoTwo:input_type -> gateway_api_conformance.echo_basic.grpcecho.EchoRequest + 4, // 7: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho.EchoThree:input_type -> gateway_api_conformance.echo_basic.grpcecho.EchoRequest + 5, // 8: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho.Echo:output_type -> gateway_api_conformance.echo_basic.grpcecho.EchoResponse + 5, // 9: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho.EchoTwo:output_type -> gateway_api_conformance.echo_basic.grpcecho.EchoResponse + 5, // 10: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho.EchoThree:output_type -> gateway_api_conformance.echo_basic.grpcecho.EchoResponse + 8, // [8:11] is the sub-list for method output_type + 5, // [5:8] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_grpcecho_proto_init() } +func file_grpcecho_proto_init() { + if File_grpcecho_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_grpcecho_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Header); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_grpcecho_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Context); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_grpcecho_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TLSAssertions); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_grpcecho_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Assertions); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_grpcecho_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EchoRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_grpcecho_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EchoResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_grpcecho_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_grpcecho_proto_goTypes, + DependencyIndexes: file_grpcecho_proto_depIdxs, + MessageInfos: file_grpcecho_proto_msgTypes, + }.Build() + File_grpcecho_proto = out.File + file_grpcecho_proto_rawDesc = nil + file_grpcecho_proto_goTypes = nil + file_grpcecho_proto_depIdxs = nil +} diff --git a/conformance/echo-basic/grpcechoserver/grpcecho_grpc.pb.go b/conformance/echo-basic/grpcechoserver/grpcecho_grpc.pb.go new file mode 100644 index 0000000000..f367cb6651 --- /dev/null +++ b/conformance/echo-basic/grpcechoserver/grpcecho_grpc.pb.go @@ -0,0 +1,183 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v4.22.2 +// source: grpcecho.proto + +package grpcechoserver + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// GrpcEchoClient is the client API for GrpcEcho service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GrpcEchoClient interface { + Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) + // Behaves identically to Echo, but lives at a different method to + // emulate the service having more than one method. + EchoTwo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) + // An intentionally unimplemented method. + EchoThree(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) +} + +type grpcEchoClient struct { + cc grpc.ClientConnInterface +} + +func NewGrpcEchoClient(cc grpc.ClientConnInterface) GrpcEchoClient { + return &grpcEchoClient{cc} +} + +func (c *grpcEchoClient) Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) { + out := new(EchoResponse) + err := c.cc.Invoke(ctx, "/gateway_api_conformance.echo_basic.grpcecho.GrpcEcho/Echo", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *grpcEchoClient) EchoTwo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) { + out := new(EchoResponse) + err := c.cc.Invoke(ctx, "/gateway_api_conformance.echo_basic.grpcecho.GrpcEcho/EchoTwo", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *grpcEchoClient) EchoThree(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) { + out := new(EchoResponse) + err := c.cc.Invoke(ctx, "/gateway_api_conformance.echo_basic.grpcecho.GrpcEcho/EchoThree", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GrpcEchoServer is the server API for GrpcEcho service. +// All implementations must embed UnimplementedGrpcEchoServer +// for forward compatibility +type GrpcEchoServer interface { + Echo(context.Context, *EchoRequest) (*EchoResponse, error) + // Behaves identically to Echo, but lives at a different method to + // emulate the service having more than one method. + EchoTwo(context.Context, *EchoRequest) (*EchoResponse, error) + // An intentionally unimplemented method. + EchoThree(context.Context, *EchoRequest) (*EchoResponse, error) + mustEmbedUnimplementedGrpcEchoServer() +} + +// UnimplementedGrpcEchoServer must be embedded to have forward compatible implementations. +type UnimplementedGrpcEchoServer struct { +} + +func (UnimplementedGrpcEchoServer) Echo(context.Context, *EchoRequest) (*EchoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Echo not implemented") +} +func (UnimplementedGrpcEchoServer) EchoTwo(context.Context, *EchoRequest) (*EchoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method EchoTwo not implemented") +} +func (UnimplementedGrpcEchoServer) EchoThree(context.Context, *EchoRequest) (*EchoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method EchoThree not implemented") +} +func (UnimplementedGrpcEchoServer) mustEmbedUnimplementedGrpcEchoServer() {} + +// UnsafeGrpcEchoServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GrpcEchoServer will +// result in compilation errors. +type UnsafeGrpcEchoServer interface { + mustEmbedUnimplementedGrpcEchoServer() +} + +func RegisterGrpcEchoServer(s grpc.ServiceRegistrar, srv GrpcEchoServer) { + s.RegisterService(&GrpcEcho_ServiceDesc, srv) +} + +func _GrpcEcho_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EchoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GrpcEchoServer).Echo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gateway_api_conformance.echo_basic.grpcecho.GrpcEcho/Echo", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GrpcEchoServer).Echo(ctx, req.(*EchoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GrpcEcho_EchoTwo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EchoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GrpcEchoServer).EchoTwo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gateway_api_conformance.echo_basic.grpcecho.GrpcEcho/EchoTwo", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GrpcEchoServer).EchoTwo(ctx, req.(*EchoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GrpcEcho_EchoThree_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EchoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GrpcEchoServer).EchoThree(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gateway_api_conformance.echo_basic.grpcecho.GrpcEcho/EchoThree", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GrpcEchoServer).EchoThree(ctx, req.(*EchoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// GrpcEcho_ServiceDesc is the grpc.ServiceDesc for GrpcEcho service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GrpcEcho_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "gateway_api_conformance.echo_basic.grpcecho.GrpcEcho", + HandlerType: (*GrpcEchoServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Echo", + Handler: _GrpcEcho_Echo_Handler, + }, + { + MethodName: "EchoTwo", + Handler: _GrpcEcho_EchoTwo_Handler, + }, + { + MethodName: "EchoThree", + Handler: _GrpcEcho_EchoThree_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "grpcecho.proto", +} diff --git a/conformance/tests/grpcroute-exact-method-matching.go b/conformance/tests/grpcroute-exact-method-matching.go new file mode 100644 index 0000000000..0310dd8b10 --- /dev/null +++ b/conformance/tests/grpcroute-exact-method-matching.go @@ -0,0 +1,76 @@ +/* +Copyright 2024 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" + + "google.golang.org/grpc/codes" + "k8s.io/apimachinery/pkg/types" + + pb "sigs.k8s.io/gateway-api/conformance/echo-basic/grpcechoserver" + + "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/conformance/utils/grpc" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, GRPCExactMethodMatching) +} + +var GRPCExactMethodMatching = suite.ConformanceTest{ + ShortName: "GRPCExactMethodMatching", + Description: "A single GRPCRoute with exact method matching for different backends", + Manifests: []string{"tests/grpcroute-exact-method-matching.yaml"}, + Features: []suite.SupportedFeature{ + suite.SupportGateway, + suite.SupportGRPCRoute, + }, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "exact-matching", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1alpha2.GRPCRoute{}, routeNN) + + testCases := []grpc.ExpectedResponse{ + { + EchoRequest: &pb.EchoRequest{}, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + }, { + EchoTwoRequest: &pb.EchoRequest{}, + Backend: "grpc-infra-backend-v2", + Namespace: ns, + }, { + EchoThreeRequest: &pb.EchoRequest{}, + Response: grpc.Response{Code: codes.Unimplemented}, + }, + } + + for i := range testCases { + // Declare tc here to avoid loop variable + // reuse issues across parallel tests. + tc := testCases[i] + t.Run(tc.GetTestCaseName(i), func(t *testing.T) { + t.Parallel() + grpc.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.TimeoutConfig, gwAddr, tc) + }) + } + }, +} diff --git a/conformance/tests/grpcroute-exact-method-matching.yaml b/conformance/tests/grpcroute-exact-method-matching.yaml new file mode 100644 index 0000000000..214a5a3d74 --- /dev/null +++ b/conformance/tests/grpcroute-exact-method-matching.yaml @@ -0,0 +1,23 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: GRPCRoute +metadata: + name: exact-matching + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: Echo + backendRefs: + - name: grpc-infra-backend-v1 + port: 8080 + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: EchoTwo + backendRefs: + - name: grpc-infra-backend-v2 + port: 8080 diff --git a/conformance/tests/grpcroute-header-matching.go b/conformance/tests/grpcroute-header-matching.go new file mode 100644 index 0000000000..297fbb6b16 --- /dev/null +++ b/conformance/tests/grpcroute-header-matching.go @@ -0,0 +1,136 @@ +/* +Copyright 2024 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" + + "google.golang.org/grpc/codes" + "k8s.io/apimachinery/pkg/types" + + pb "sigs.k8s.io/gateway-api/conformance/echo-basic/grpcechoserver" + + "sigs.k8s.io/gateway-api/conformance/utils/grpc" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, GRPCRouteHeaderMatching) +} + +var GRPCRouteHeaderMatching = suite.ConformanceTest{ + ShortName: "GRPCRouteHeaderMatching", + Description: "A single GRPCRoute with header matching for different backends", + Manifests: []string{"tests/grpcroute-header-matching.yaml"}, + Features: []suite.SupportedFeature{ + suite.SupportGateway, + suite.SupportGRPCRoute, + }, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "header-matching", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + testCases := []grpc.ExpectedResponse{{ + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{"Version": "one"}, + }, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{"Version": "two"}, + }, + Backend: "grpc-infra-backend-v2", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{"Version": "two", "Color": "orange"}, + }, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{"Version": "two", "Color": "blue"}, + }, + Backend: "grpc-infra-backend-v2", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{"Color": "orange"}, + }, + Response: grpc.Response{Code: codes.Unimplemented}, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{"Some-Other-Header": "one"}, + }, + Response: grpc.Response{Code: codes.Unimplemented}, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{"Color": "blue"}, + }, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{"Color": "green"}, + }, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{"Color": "red"}, + }, + Backend: "grpc-infra-backend-v2", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{"Color": "yellow"}, + }, + Backend: "grpc-infra-backend-v2", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{"Color": "purple"}, + }, + Response: grpc.Response{Code: codes.Unimplemented}, + }} + + for i := range testCases { + // Declare tc here to avoid loop variable + // reuse issues across parallel tests. + tc := testCases[i] + t.Run(tc.GetTestCaseName(i), func(t *testing.T) { + t.Parallel() + grpc.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.TimeoutConfig, gwAddr, tc) + }) + } + }, +} diff --git a/conformance/tests/grpcroute-header-matching.yaml b/conformance/tests/grpcroute-header-matching.yaml new file mode 100644 index 0000000000..3f5ea742e5 --- /dev/null +++ b/conformance/tests/grpcroute-header-matching.yaml @@ -0,0 +1,57 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: GRPCRoute +metadata: + name: grpc-header-matching + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + # Matches "version: one" + - matches: + - headers: + - name: version + value: one + backendRefs: + - name: grpc-infra-backend-v1 + port: 8080 + # Matches "version: two" + - matches: + - headers: + - name: version + value: two + backendRefs: + - name: grpc-infra-backend-v2 + port: 8080 + # Matches "version: two" AND "color: orange" + - matches: + - headers: + - name: version + value: two + - name: color + value: orange + backendRefs: + - name: grpc-infra-backend-v1 + port: 8080 + # Matches "color: blue" OR "color: green" + - matches: + - headers: + - name: color + value: blue + - headers: + - name: color + value: green + backendRefs: + - name: grpc-infra-backend-v1 + port: 8080 + # Matches "color: red" OR "color: yellow" + - matches: + - headers: + - name: color + value: red + - headers: + - name: color + value: yellow + backendRefs: + - name: grpc-infra-backend-v2 + port: 8080 diff --git a/conformance/tests/grpcroute-listener-hostname-matching.go b/conformance/tests/grpcroute-listener-hostname-matching.go new file mode 100644 index 0000000000..34e4c17133 --- /dev/null +++ b/conformance/tests/grpcroute-listener-hostname-matching.go @@ -0,0 +1,129 @@ +/* +Copyright 2024 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" + + "google.golang.org/grpc/codes" + "k8s.io/apimachinery/pkg/types" + + pb "sigs.k8s.io/gateway-api/conformance/echo-basic/grpcechoserver" + + "sigs.k8s.io/gateway-api/conformance/utils/grpc" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, GRPCRouteListenerHostnameMatching) +} + +var GRPCRouteListenerHostnameMatching = suite.ConformanceTest{ + ShortName: "GRPCRouteListenerHostnameMatching", + Description: "Multiple GRPC listeners with the same port and different hostnames, each with a different GRPCRoute", + Manifests: []string{"tests/grpcroute-listener-hostname-matching.yaml"}, + Features: []suite.SupportedFeature{ + suite.SupportGateway, + suite.SupportGRPCRoute, + }, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + + // This test creates an additional Gateway in the gateway-conformance-infra + // namespace so we have to wait for it to be ready. + kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns}) + + gwNN := types.NamespacedName{Name: "grpcroute-listener-hostname-matching", Namespace: ns} + + _ = kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN, "listener-1"), + types.NamespacedName{Namespace: ns, Name: "backend-v1"}, + ) + _ = kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN, "listener-2"), + types.NamespacedName{Namespace: ns, Name: "backend-v2"}, + ) + gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN, "listener-3", "listener-4"), + types.NamespacedName{Namespace: ns, Name: "backend-v3"}, + ) + + testCases := []grpc.ExpectedResponse{{ + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "bar.com", + }, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "foo.bar.com", + }, + Backend: "grpc-infra-backend-v2", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "baz.bar.com", + }, + Backend: "grpc-infra-backend-v3", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "boo.bar.com", + }, + Backend: "grpc-infra-backend-v3", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "multiple.prefixes.bar.com", + }, + Backend: "grpc-infra-backend-v3", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "multiple.prefixes.foo.com", + }, + Backend: "grpc-infra-backend-v3", + Namespace: ns, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "foo.com", + }, + Response: grpc.Response{Code: codes.Unimplemented}, + }, { + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "no.matching.host", + }, + Response: grpc.Response{Code: codes.Unimplemented}, + }} + + for i := range testCases { + // Declare tc here to avoid loop variable + // reuse issues across parallel tests. + tc := testCases[i] + t.Run(tc.GetTestCaseName(i), func(t *testing.T) { + t.Parallel() + grpc.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.TimeoutConfig, gwAddr, tc) + }) + } + }, +} diff --git a/conformance/tests/grpcroute-listener-hostname-matching.yaml b/conformance/tests/grpcroute-listener-hostname-matching.yaml new file mode 100644 index 0000000000..6232b36f5e --- /dev/null +++ b/conformance/tests/grpcroute-listener-hostname-matching.yaml @@ -0,0 +1,84 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: grpcroute-listener-hostname-matching + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: listener-1 + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + hostname: bar.com + - name: listener-2 + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + hostname: foo.bar.com + - name: listener-3 + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + hostname: "*.bar.com" + - name: listener-4 + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + hostname: "*.foo.com" +--- +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: GRPCRoute +metadata: + name: backend-v1 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: grpcroute-listener-hostname-matching + namespace: gateway-conformance-infra + sectionName: listener-1 + rules: + - backendRefs: + - name: grpc-infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: GRPCRoute +metadata: + name: backend-v2 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: grpcroute-listener-hostname-matching + namespace: gateway-conformance-infra + sectionName: listener-2 + rules: + - backendRefs: + - name: grpc-infra-backend-v2 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: GRPCRoute +metadata: + name: backend-v3 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: grpcroute-listener-hostname-matching + namespace: gateway-conformance-infra + sectionName: listener-3 + - name: grpcroute-listener-hostname-matching + namespace: gateway-conformance-infra + sectionName: listener-4 + rules: + - backendRefs: + - name: grpc-infra-backend-v3 + port: 8080 diff --git a/conformance/utils/grpc/grpc.go b/conformance/utils/grpc/grpc.go new file mode 100644 index 0000000000..eb71429568 --- /dev/null +++ b/conformance/utils/grpc/grpc.go @@ -0,0 +1,280 @@ +/* +Copyright 2023 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 grpc + +import ( + "context" + "fmt" + "sort" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + pb "sigs.k8s.io/gateway-api/conformance/echo-basic/grpcechoserver" + "sigs.k8s.io/gateway-api/conformance/utils/config" + "sigs.k8s.io/gateway-api/conformance/utils/http" +) + +const ( + echoServerPackage = "gateway_api_conformance.echo_basic.grpcecho" + echoServerService = "GrpcEcho" +) + +type Response struct { + Code codes.Code + Headers *metadata.MD + Trailers *metadata.MD + Response *pb.EchoResponse +} + +type RequestMetadata struct { + // The :authority pseudoheader to set on the outgoing request. + Authority string + + // Outgoing metadata pairs to add to the request. + Metadata map[string]string +} + +// ExpectedResponse defines the response expected for a given request. +type ExpectedResponse struct { + // Defines the request to make. Only one of EchoRequest and EchoTwoRequest + // may be set. + EchoRequest *pb.EchoRequest + EchoTwoRequest *pb.EchoRequest + EchoThreeRequest *pb.EchoRequest + + // Metadata describing the outgoing request. + RequestMetadata *RequestMetadata + + // Response defines what response the test case + // should receive. + Response Response + + Backend string + Namespace string + + // User Given TestCase name + TestCaseName string +} + +func getMethodName(expected *ExpectedResponse) string { + switch { + case expected.EchoRequest != nil: + return "Echo" + case expected.EchoTwoRequest != nil: + return "EchoTwo" + default: + return "EchoThree" + } +} + +func getFullyQualifiedMethod(expected *ExpectedResponse) string { + return fmt.Sprintf("/%s.%s/%s", echoServerPackage, echoServerService, getMethodName(expected)) +} + +func getMapDeterministicStr(m map[string]string) string { + keys := []string{} + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + out := "{" + for i, key := range keys { + out += key + ":" + m[key] + if i != len(keys)-1 { + out += "," + } + } + out += "}" + return out +} + +func (er *ExpectedResponse) GetTestCaseName(i int) string { + if er.TestCaseName != "" { + return er.TestCaseName + } + + headerStr := "" + reqStr := "" + + authority := "" + if er.RequestMetadata != nil { + rm := er.RequestMetadata + authority = rm.Authority + if len(rm.Metadata) > 0 { + headerStr = fmt.Sprintf(" with headers '%s'", getMapDeterministicStr(rm.Metadata)) + } + } + + reqStr = fmt.Sprintf("%d request to '%s%s'%s", i, authority, getFullyQualifiedMethod(er), headerStr) + + if er.Backend != "" { + return fmt.Sprintf("%s should go to %s", reqStr, er.Backend) + } + return fmt.Sprintf("%s should receive a %s (%d)", reqStr, er.Response.Code.String(), er.Response.Code) +} + +type client struct { + Conn *grpc.ClientConn + RequestMetadata *RequestMetadata +} + +func (c *client) ensureConnection(address string) error { + if c.Conn != nil { + return nil + } + var err error + dialOpts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} + if c.RequestMetadata != nil && c.RequestMetadata.Authority != "" { + dialOpts = append(dialOpts, grpc.WithAuthority(c.RequestMetadata.Authority)) + } + + c.Conn, err = grpc.Dial(address, dialOpts...) + if err != nil { + c.Conn = nil + return err + } + return nil +} + +func (c *client) resetConnection() { + if c.Conn == nil { + return + } + c.Conn.Close() + c.Conn = nil +} + +func (c *client) SendRPC(t *testing.T, address string, expected ExpectedResponse, timeout time.Duration) (*Response, error) { + t.Helper() + if err := c.ensureConnection(address); err != nil { + return &Response{}, err + } + + resp := &Response{ + Headers: &metadata.MD{}, + Trailers: &metadata.MD{}, + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + + if c.RequestMetadata != nil && len(c.RequestMetadata.Metadata) > 0 { + ctx = metadata.NewOutgoingContext(ctx, metadata.New(c.RequestMetadata.Metadata)) + } + + defer cancel() + + stub := pb.NewGrpcEchoClient(c.Conn) + var err error + t.Logf("Sending RPC") + + switch { + case expected.EchoRequest != nil: + resp.Response, err = stub.Echo(ctx, expected.EchoRequest, grpc.Header(resp.Headers), grpc.Trailer(resp.Trailers)) + case expected.EchoTwoRequest != nil: + resp.Response, err = stub.EchoTwo(ctx, expected.EchoTwoRequest, grpc.Header(resp.Headers), grpc.Trailer(resp.Trailers)) + case expected.EchoThreeRequest != nil: + resp.Response, err = stub.EchoThree(ctx, expected.EchoThreeRequest, grpc.Header(resp.Headers), grpc.Trailer(resp.Trailers)) + default: + return resp, fmt.Errorf("no request specified") + } + + if err != nil { + resp.Code = status.Code(err) + t.Logf("RPC finished with error: %v", err) + if resp.Code == codes.Internal { + t.Logf("Received code Internal. Resetting connection.") + c.resetConnection() + } + } else { + t.Logf("RPC finished with response %v", resp.Response) + resp.Code = codes.OK + } + + return resp, nil +} + +func (c *client) Close() { + if c.Conn != nil { + c.Conn.Close() + } +} + +func compareResponse(expected *ExpectedResponse, response *Response) error { + if expected.Response.Code != response.Code { + return fmt.Errorf("expected status code to be %s (%d), but got %s (%d)", expected.Response.Code.String(), expected.Response.Code, response.Code.String(), response.Code) + } + if response.Code == codes.OK { + expectedFullyQualifiedMethod := getFullyQualifiedMethod(expected) + if expectedFullyQualifiedMethod != response.Response.GetAssertions().GetFullyQualifiedMethod() { + return fmt.Errorf("expected path to be %s, got %s ", expectedFullyQualifiedMethod, response.Response.GetAssertions().GetFullyQualifiedMethod()) + } + + if expected.Namespace != "" && expected.Namespace != response.Response.GetAssertions().GetContext().GetNamespace() { + return fmt.Errorf("expected namespace to be %s, got %s", expected.Namespace, response.Response.GetAssertions().GetContext().GetNamespace()) + } + + if !strings.HasPrefix(response.Response.GetAssertions().GetContext().GetPod(), expected.Backend) { + return fmt.Errorf("expected pod name to start with %s, got %s", expected.Backend, response.Response.GetAssertions().GetContext().GetPod()) + } + } + return nil +} + +func validateExpectedResponse(t *testing.T, expected ExpectedResponse) { + requestTypeCount := 0 + if expected.EchoRequest != nil { + requestTypeCount++ + } + if expected.EchoTwoRequest != nil { + requestTypeCount++ + } + if expected.EchoThreeRequest != nil { + requestTypeCount++ + } + require.Equal(t, 1, requestTypeCount, "expected only one request type to be set, but found %d: %v", requestTypeCount, expected) +} + +func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, timeoutConfig config.TimeoutConfig, gwAddr string, expected ExpectedResponse) { + t.Helper() + validateExpectedResponse(t, expected) + c := &client{ + Conn: nil, + RequestMetadata: expected.RequestMetadata, + } + defer c.Close() + sendRPC := func(elapsed time.Duration) bool { + resp, err := c.SendRPC(t, gwAddr, expected, timeoutConfig.MaxTimeToConsistency-elapsed) + if err != nil { + t.Logf("Failed to send RPC, not ready yet: %v (after %v)", err, elapsed) + return false + } + if err := compareResponse(&expected, resp); err != nil { + t.Logf("Response expectation failed for request: %v not ready yet: %v (after %v)", expected, err, elapsed) + return false + } + return true + } + http.AwaitConvergence(t, timeoutConfig.RequiredConsecutiveSuccesses, timeoutConfig.MaxTimeToConsistency, sendRPC) + t.Logf("Request passed") +} diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go index 701c9605fa..795393683a 100644 --- a/conformance/utils/kubernetes/helpers.go +++ b/conformance/utils/kubernetes/helpers.go @@ -321,7 +321,7 @@ func MeshNamespacesMustBeReady(t *testing.T, c client.Client, timeoutConfig conf require.NoErrorf(t, waitErr, "error waiting for %s namespaces to be ready", strings.Join(namespaces, ", ")) } -// GatewayAndHTTPRoutesMustBeAccepted waits until: +// GatewayAndRoutesMustBeAccepted waits until: // 1. The specified Gateway has an IP address assigned to it. // 2. The route has a ParentRef referring to the Gateway. // 3. All the gateway's listeners have the following conditions set to true: @@ -330,7 +330,7 @@ func MeshNamespacesMustBeReady(t *testing.T, c client.Client, timeoutConfig conf // - ListenerConditionProgrammed // // The test will fail if these conditions are not met before the timeouts. -func GatewayAndHTTPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeNNs ...types.NamespacedName) string { +func GatewayAndRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeType any, routeNNs ...types.NamespacedName) string { t.Helper() gwAddr, err := WaitForGatewayAddress(t, c, timeoutConfig, gw.NamespacedName) @@ -363,7 +363,7 @@ func GatewayAndHTTPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutCo }}, }) } - HTTPRouteMustHaveParents(t, c, timeoutConfig, routeNN, parents, namespaceRequired) + RouteMustHaveParents(t, c, timeoutConfig, routeNN, parents, namespaceRequired, routeType) } requiredListenerConditions := []metav1.Condition{ @@ -388,6 +388,19 @@ func GatewayAndHTTPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutCo return gwAddr } +// GatewayAndHTTPRoutesMustBeAccepted waits until: +// 1. The specified Gateway has an IP address assigned to it. +// 2. The route has a ParentRef referring to the Gateway. +// 3. All the gateway's listeners have the following conditions set to true: +// - ListenerConditionResolvedRefs +// - ListenerConditionAccepted +// - ListenerConditionProgrammed +// +// The test will fail if these conditions are not met before the timeouts. +func GatewayAndHTTPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeNNs ...types.NamespacedName) string { + return GatewayAndRoutesMustBeAccepted(t, c, timeoutConfig, controllerName, gw, &gatewayv1.HTTPRoute{}, routeNNs...) +} + // WaitForGatewayAddress waits until at least one IP Address has been set in the // status of the specified Gateway. func WaitForGatewayAddress(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, gwName types.NamespacedName) (string, error) { @@ -528,31 +541,62 @@ func HTTPRouteMustHaveNoAcceptedParents(t *testing.T, client client.Client, time require.NoErrorf(t, waitErr, "error waiting for HTTPRoute to have no accepted parents") } -// HTTPRouteMustHaveParents waits for the specified HTTPRoute to have parents -// in status that match the expected parents. This will cause the test to halt -// if the specified timeout is exceeded. -func HTTPRouteMustHaveParents(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, routeName types.NamespacedName, parents []gatewayv1.RouteParentStatus, namespaceRequired bool) { +// RouteTypeMustHaveParentsField ensures the provided routeType has a +// routeType.Status.Parents field of type []v1alpha2.RouteParentStatus. +func RouteTypeMustHaveParentsField(t *testing.T, routeType any) string { + t.Helper() + routeTypePointerObj := reflect.TypeOf(routeType) + require.Equal(t, reflect.Pointer, routeTypePointerObj.Kind()) + + routeTypeObj := routeTypePointerObj.Elem() + routeTypeName := routeTypeObj.Name() + + statusField, ok := routeTypeObj.FieldByName("Status") + require.True(t, ok, "%s does not have a Status field", routeTypeName) + + parentsField, ok := statusField.Type.FieldByName("Parents") + require.True(t, ok, "%s.Status does not have a Parents field", routeTypeName) + require.Equal(t, parentsField.Type, reflect.TypeOf([]v1alpha2.RouteParentStatus{})) + + return routeTypeName +} + +func RouteMustHaveParents(t *testing.T, cli client.Client, timeoutConfig config.TimeoutConfig, routeName types.NamespacedName, parents []gatewayv1.RouteParentStatus, namespaceRequired bool, routeType any) { t.Helper() + routeTypeName := RouteTypeMustHaveParentsField(t, routeType) + + cliObj, ok := routeType.(client.Object) + require.True(t, ok, "error converting %v to client.Object", routeType) + + metaObj, ok := routeType.(metav1.Object) + require.True(t, ok, "error converting %v to metav1.Object", routeType) + var actual []gatewayv1.RouteParentStatus waitErr := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, timeoutConfig.RouteMustHaveParents, true, func(ctx context.Context) (bool, error) { - route := &gatewayv1.HTTPRoute{} - err := client.Get(ctx, routeName, route) + err := cli.Get(ctx, routeName, cliObj) if err != nil { - return false, fmt.Errorf("error fetching HTTPRoute: %w", err) + return false, fmt.Errorf("error fetching %s: %w", routeTypeName, err) } for _, parent := range actual { - if err := ConditionsHaveLatestObservedGeneration(route, parent.Conditions); err != nil { - t.Logf("HTTPRoute(controller=%v,ref=%#v) %v", parent.ControllerName, parent, err) + if err := ConditionsHaveLatestObservedGeneration(metaObj, parent.Conditions); err != nil { + t.Logf("%s(controller=%v,ref=%#v) %v", routeTypeName, parent.ControllerName, parent, err) return false, nil } } - actual = route.Status.Parents + actual = reflect.ValueOf(cliObj).Elem().FieldByName("Status").FieldByName("Parents").Interface().([]v1alpha2.RouteParentStatus) return parentsForRouteMatch(t, routeName, parents, actual, namespaceRequired), nil }) - require.NoErrorf(t, waitErr, "error waiting for HTTPRoute to have parents matching expectations") + require.NoErrorf(t, waitErr, "error waiting for %s to have parents matching expectations", routeTypeName) +} + +// HTTPRouteMustHaveParents waits for the specified HTTPRoute to have parents +// in status that match the expected parents. This will cause the test to halt +// if the specified timeout is exceeded. +func HTTPRouteMustHaveParents(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, routeName types.NamespacedName, parents []gatewayv1.RouteParentStatus, namespaceRequired bool) { + RouteMustHaveParents(t, client, timeoutConfig, routeName, parents, namespaceRequired, &gatewayv1.HTTPRoute{}) } // TLSRouteMustHaveParents waits for the specified TLSRoute to have parents diff --git a/conformance/utils/suite/features.go b/conformance/utils/suite/features.go index 79a0cc74b3..e1e204af8a 100644 --- a/conformance/utils/suite/features.go +++ b/conformance/utils/suite/features.go @@ -211,6 +211,21 @@ var MeshCoreFeatures = sets.New( SupportMesh, ) +// ----------------------------------------------------------------------------- +// Features - GRPCRoute Conformance (Experimental) +// ----------------------------------------------------------------------------- + +const ( + // This option indicates general support for service mesh + SupportGRPCRoute SupportedFeature = "GRPCRoute" +) + +// GRPCRouteCoreFeatures includes all the supported features for GRPCRoute at +// a Core level of support. +var GRPCRouteCoreFeatures = sets.New( + SupportGRPCRoute, +) + // ----------------------------------------------------------------------------- // Features - Compilations // ----------------------------------------------------------------------------- @@ -227,4 +242,5 @@ var AllFeatures = sets.New[SupportedFeature](). Insert(HTTPRouteExtendedFeatures.UnsortedList()...). Insert(HTTPRouteExperimentalFeatures.UnsortedList()...). Insert(TLSRouteCoreFeatures.UnsortedList()...). - Insert(MeshCoreFeatures.UnsortedList()...) + Insert(MeshCoreFeatures.UnsortedList()...). + Insert(GRPCRouteCoreFeatures.UnsortedList()...) diff --git a/docker/Dockerfile.echo-basic b/docker/Dockerfile.echo-basic index a923253a6e..0aae73a3df 100644 --- a/docker/Dockerfile.echo-basic +++ b/docker/Dockerfile.echo-basic @@ -21,6 +21,12 @@ WORKDIR /go/src/sigs.k8s.io/gateway-api/conformance/echo-basic COPY ./conformance/echo-basic ./ +# If left as go.mod and go.sum in the external repo, these files would +# interfere with the ability to use reuse the protobuf/gRPC generated code +# for the test client in the conformance tests. +RUN mv .go.mod go.mod +RUN mv .go.sum go.sum + RUN go build -trimpath -ldflags="-buildid= -s -w" -o echo-basic . # Use distroless as minimal base image to package the binary diff --git a/go.mod b/go.mod index bfa6586842..61c6d8e7d6 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( github.com/ahmetb/gen-crd-api-reference-docs v0.3.0 github.com/stretchr/testify v1.8.4 golang.org/x/net v0.20.0 + google.golang.org/grpc v1.61.0 + google.golang.org/protobuf v1.32.0 k8s.io/api v0.29.2 k8s.io/apiextensions-apiserver v0.29.2 k8s.io/apimachinery v0.29.2 @@ -66,7 +68,7 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.16.1 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.32.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 3ed1009a1b..cf15911325 100644 --- a/go.sum +++ b/go.sum @@ -210,6 +210,10 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= diff --git a/hack/boilerplate/boilerplate.py b/hack/boilerplate/boilerplate.py index 73c7e0d158..7c8ed82825 100755 --- a/hack/boilerplate/boilerplate.py +++ b/hack/boilerplate/boilerplate.py @@ -168,6 +168,9 @@ def file_extension(filename): 'third_party', 'vendor', '.venv', + + # These files are generated by an external generator. + 'conformance/echo-basic/grpcechoserver', ] # list all the files contain 'DO NOT EDIT', but are not generated diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 1c0d7aee7f..25fc42a72a 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -97,3 +97,77 @@ do paths="${APIS_PKG}/apis/${VERSION}" done + +echo "Generating gRPC/Protobuf code" + +readonly PROTOC_CACHE_DIR="/tmp/protoc.cache" +readonly PROTOC_BINARY="${PROTOC_CACHE_DIR}/bin/protoc" +readonly PROTOC_VERSION="22.2" +readonly PROTOC_REPO="https://github.com/protocolbuffers/protobuf" + +readonly PROTOC_LINUX_X86_URL="${PROTOC_REPO}/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip" +readonly PROTOC_LINUX_X86_CHECKSUM="4805ba56594556402a6c327a8d885a47640ee363 ${PROTOC_BINARY}" + +readonly PROTOC_LINUX_ARM64_URL="${PROTOC_REPO}/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-aarch_64.zip" +readonly PROTOC_LINUX_ARM63_CHECKSUM="47285b2386f990da319e9eef92cadec2dfa28733 ${PROTOC_BINARY}" + +readonly PROTOC_MAC_UNIVERSAL_URL="${PROTOC_REPO}/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-osx-universal_binary.zip" +readonly PROTOC_MAC_UNIVERSAL_CHECKSUM="2a79d0eb235c808eca8de893762072b94dc6144c ${PROTOC_BINARY}" + +PROTOC_URL="" +PROTOC_CHECKSUM="" + +ARCH=$(uname -m) +RAW_OS=$(uname -o) + +OS="" +if echo "${RAW_OS}" | grep -i "Linux" >/dev/null; then + OS="Linux" +elif echo "${RAW_OS}" | grep -i "Darwin" >/dev/null; then + OS="Mac" +else + echo "Unsupported operating system" +fi + +if [[ "${OS}" == "Linux" ]]; then + if [[ "$ARCH" == "x86_64" ]]; then + PROTOC_URL="$PROTOC_LINUX_X86_URL" + PROTOC_CHECKSUM="$PROTOC_LINUX_X86_CHECKSUM" + elif [[ "$ARCH" == "arm64" ]]; then + PROTOC_URL="$PROTOC_LINUX_ARM64_URL" + PROTOC_CHECKSUM="$PROTOC_LINUX_ARM64_CHECKSUM" + else + echo "Architecture ${ARCH} is not supported on OS ${OS}." >/dev/stderr + exit 1 + fi +elif [[ "${OS}" == "Mac" ]]; then + PROTOC_URL="$PROTOC_MAC_UNIVERSAL_URL" + PROTOC_CHECKSUM="$PROTOC_MAC_UNIVERSAL_CHECKSUM" +fi + +function verify_protoc { + if ! echo "${PROTOC_CHECKSUM}" | shasum -c >/dev/null; then + echo "Downloaded protoc binary failed checksum." >/dev/stderr + exit 1 + fi +} + +function ensure_protoc { + mkdir -p "${PROTOC_CACHE_DIR}" + if [ ! -f "${PROTOC_BINARY}" ]; then + curl -sL -o "${PROTOC_CACHE_DIR}/protoc.zip" "${PROTOC_URL}" + unzip -d "${PROTOC_CACHE_DIR}" "${PROTOC_CACHE_DIR}/protoc.zip" + fi + verify_protoc +} + +ensure_protoc +go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 + +(cd conformance/echo-basic && \ + export PATH="$PATH:$GOPATH/bin" && \ + "${PROTOC_BINARY}" --go_out=grpcechoserver --go_opt=paths=source_relative \ + --go-grpc_out=grpcechoserver --go-grpc_opt=paths=source_relative \ + grpcecho.proto +)