diff --git a/test/crd.go b/test/crd.go index 5a8cfe225f73..896a8681ea37 100644 --- a/test/crd.go +++ b/test/crd.go @@ -45,7 +45,7 @@ func Route(namespace string, route string, config string) *v1alpha1.Route { // Configuration returns a Configuration object in namespace with the name config // that uses the image specifed by imagePath. -func Configuration(namespace string, config string, imagePath string) *v1alpha1.Configuration { +func Configuration(namespace string, config string, imagePath string, protocol v1alpha1.RevisionProtocolType) *v1alpha1.Configuration { return &v1alpha1.Configuration{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, @@ -54,6 +54,7 @@ func Configuration(namespace string, config string, imagePath string) *v1alpha1. Spec: v1alpha1.ConfigurationSpec{ RevisionTemplate: v1alpha1.RevisionTemplateSpec{ Spec: v1alpha1.RevisionSpec{ + Protocol: protocol, Container: corev1.Container{ Image: imagePath, }, diff --git a/test/e2e/autoscale_test.go b/test/e2e/autoscale_test.go index dc49d183874a..eb32650e8f9c 100644 --- a/test/e2e/autoscale_test.go +++ b/test/e2e/autoscale_test.go @@ -82,7 +82,7 @@ func TestAutoscaleUpDownUp(t *testing.T) { "/") log.Println("Creating a new Route and Configuration") - err := CreateRouteAndConfig(clients, imagePath) + err := CreateRouteAndConfig(clients, imagePath, v1alpha1.RevisionProtocolHTTP) if err != nil { t.Fatalf("Failed to create Route and Configuration: %v", err) } diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 10bc3387cdfa..2d48cea8ec33 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -3,6 +3,7 @@ package e2e import ( "testing" + "github.com/knative/serving/pkg/apis/ela/v1alpha1" "github.com/knative/serving/test" // Mysteriously required to support GCP auth (required by k8s libs). // Apparently just importing it is enough. @_@ side effects @_@. @@ -34,9 +35,9 @@ func TearDown(clients *test.Clients) { } } -func CreateRouteAndConfig(clients *test.Clients, imagePath string) error { +func CreateRouteAndConfig(clients *test.Clients, imagePath string, protocol v1alpha1.RevisionProtocolType) error { _, err := clients.Configs.Create( - test.Configuration(NamespaceName, ConfigName, imagePath)) + test.Configuration(NamespaceName, ConfigName, imagePath, protocol)) if err != nil { return err } diff --git a/test/e2e/helloworld_test.go b/test/e2e/helloworld_test.go index 268f83e02316..c07f7f085aa8 100644 --- a/test/e2e/helloworld_test.go +++ b/test/e2e/helloworld_test.go @@ -16,13 +16,19 @@ limitations under the License. package e2e import ( + "context" + "fmt" "log" "strings" "testing" + "time" "github.com/knative/serving/pkg/apis/serving/v1alpha1" "github.com/knative/serving/test" + hello "github.com/knative/serving/test/e2e/test_images/helloworld-grpc/proto" + "google.golang.org/grpc" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" ) const ( @@ -44,7 +50,7 @@ func TestHelloWorld(t *testing.T) { imagePath = strings.Join([]string{test.Flags.DockerRepo, "helloworld"}, "/") log.Println("Creating a new Route and Configuration") - err := CreateRouteAndConfig(clients, imagePath) + err := CreateRouteAndConfig(clients, imagePath, v1alpha1.RevisionProtocolHTTP) if err != nil { t.Fatalf("Failed to create Route and Configuration: %v", err) } @@ -67,3 +73,75 @@ func TestHelloWorld(t *testing.T) { t.Fatalf("The endpoint for Route %s at domain %s didn't serve the expected text \"%s\": %v", RouteName, domain, helloWorldExpectedOutput, err) } } + +func TestHelloWorldGRPC(t *testing.T) { + clients := Setup(t) + defer TearDown(clients) + test.CleanupOnInterrupt(func() { TearDown(clients) }) + + var imagePath string + imagePath = strings.Join([]string{test.Flags.DockerRepo, "helloworld-grpc"}, "/") + + log.Println("Creating a new Route and Configuration") + err := CreateRouteAndConfig(clients, imagePath, v1alpha1.RevisionProtocolGRPC) + if err != nil { + t.Fatalf("Failed to create Route and Configuration: %v", err) + } + + log.Println("When the Revision can have traffic routed to it, the Route is marked as Ready.") + err = test.WaitForRouteState(clients.Routes, RouteName, func(r *v1alpha1.Route) (bool, error) { + return r.Status.IsReady(), nil + }) + if err != nil { + t.Fatalf("The Route %s was not marked as Ready to serve traffic: %v", RouteName, err) + } + + route, err := clients.Routes.Get(RouteName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Error fetching Route %s: %v", RouteName, err) + } + domain := route.Status.Domain + + endpoint, spoofDomain, err := test.FetchEndpointDomain(clients.Kube, test.Flags.ResolvableDomain, domain, NamespaceName, RouteName) + if err != nil { + t.Fatalf("Error fetching endpoint domains: %v", err) + } + + ingressAddress := fmt.Sprintf("%s:%d", strings.TrimPrefix(endpoint, "http://"), 80) + err = waitForHelloWorldGRPCEndpoint(ingressAddress, spoofDomain) + if err != nil { + t.Fatal(err) + } +} + +func waitForHelloWorldGRPCEndpoint(address string, spoofDomain string) error { + opts := []grpc.DialOption{ + grpc.WithInsecure(), + } + if spoofDomain != "" { + opts = append(opts, grpc.WithAuthority(spoofDomain)) + } + + err := wait.PollImmediate(test.RequestInterval, test.RequestTimeout, func() (bool, error) { + conn, _ := grpc.Dial(address, opts...) + defer conn.Close() + + client := hello.NewHelloServiceClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + resp, err := client.Hello(ctx, &hello.Request{Msg: "world"}) + if err != nil { + return true, err + } + + expectedResponse := "Hello world" + receivedResponse := resp.GetMsg() + + if receivedResponse == expectedResponse { + return true, nil + } + return false, fmt.Errorf("Did not get expected response message %s, got %s", expectedResponse, receivedResponse) + }) + return err +} diff --git a/test/e2e/test_images/helloworld-grpc/Dockerfile b/test/e2e/test_images/helloworld-grpc/Dockerfile new file mode 100644 index 000000000000..61bd3e4dd158 --- /dev/null +++ b/test/e2e/test_images/helloworld-grpc/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:latest + +ENV APP ${GOPATH}/src/github.com/elafros/elafros/test/e2e/test_images/helloworld-grpc + +RUN mkdir -p "$APP" +ADD . "$APP" +RUN go get google.golang.org/grpc && \ + go get golang.org/x/net/context + +WORKDIR "$APP" +RUN go build -o /app/helloworld-grpc . +CMD ["/app/helloworld-grpc"] diff --git a/test/e2e/test_images/helloworld-grpc/helloworld.go b/test/e2e/test_images/helloworld-grpc/helloworld.go new file mode 100644 index 000000000000..408c14ef1d8f --- /dev/null +++ b/test/e2e/test_images/helloworld-grpc/helloworld.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "log" + "net" + + hello "github.com/elafros/elafros/test/e2e/test_images/helloworld-grpc/proto" + "golang.org/x/net/context" + "google.golang.org/grpc" +) + +type helloServer struct { +} + +func (s *helloServer) Hello(ctx context.Context, req *hello.Request) (*hello.Response, error) { + return &hello.Response{Msg: fmt.Sprintf("Hello %s", req.Msg)}, nil +} + +func main() { + lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", 8080)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + helloServer := &helloServer{} + + grpcServer := grpc.NewServer() + hello.RegisterHelloServiceServer(grpcServer, helloServer) + grpcServer.Serve(lis) +} diff --git a/test/e2e/test_images/helloworld-grpc/proto/helloworld.pb.go b/test/e2e/test_images/helloworld-grpc/proto/helloworld.pb.go new file mode 100644 index 000000000000..d09338e15e96 --- /dev/null +++ b/test/e2e/test_images/helloworld-grpc/proto/helloworld.pb.go @@ -0,0 +1,157 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: helloworld.proto + +/* +Package hello is a generated protocol buffer package. + +It is generated from these files: + helloworld.proto + +It has these top-level messages: + Request + Response +*/ +package hello + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type Request struct { + Msg string `protobuf:"bytes,1,opt,name=msg" json:"msg,omitempty"` +} + +func (m *Request) Reset() { *m = Request{} } +func (m *Request) String() string { return proto.CompactTextString(m) } +func (*Request) ProtoMessage() {} +func (*Request) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *Request) GetMsg() string { + if m != nil { + return m.Msg + } + return "" +} + +type Response struct { + Msg string `protobuf:"bytes,1,opt,name=msg" json:"msg,omitempty"` +} + +func (m *Response) Reset() { *m = Response{} } +func (m *Response) String() string { return proto.CompactTextString(m) } +func (*Response) ProtoMessage() {} +func (*Response) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func (m *Response) GetMsg() string { + if m != nil { + return m.Msg + } + return "" +} + +func init() { + proto.RegisterType((*Request)(nil), "hello.Request") + proto.RegisterType((*Response)(nil), "hello.Response") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// Client API for HelloService service + +type HelloServiceClient interface { + Hello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) +} + +type helloServiceClient struct { + cc *grpc.ClientConn +} + +func NewHelloServiceClient(cc *grpc.ClientConn) HelloServiceClient { + return &helloServiceClient{cc} +} + +func (c *helloServiceClient) Hello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := grpc.Invoke(ctx, "/hello.HelloService/Hello", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for HelloService service + +type HelloServiceServer interface { + Hello(context.Context, *Request) (*Response, error) +} + +func RegisterHelloServiceServer(s *grpc.Server, srv HelloServiceServer) { + s.RegisterService(&_HelloService_serviceDesc, srv) +} + +func _HelloService_Hello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HelloServiceServer).Hello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/hello.HelloService/Hello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HelloServiceServer).Hello(ctx, req.(*Request)) + } + return interceptor(ctx, in, info, handler) +} + +var _HelloService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "hello.HelloService", + HandlerType: (*HelloServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Hello", + Handler: _HelloService_Hello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "helloworld.proto", +} + +func init() { proto.RegisterFile("helloworld.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 128 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9, + 0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x05, 0x8b, + 0x28, 0x49, 0x73, 0xb1, 0x07, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x08, 0x09, 0x70, 0x31, 0xe7, + 0x16, 0xa7, 0x4b, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06, 0x81, 0x98, 0x4a, 0x32, 0x5c, 0x1c, 0x41, + 0xa9, 0xc5, 0x05, 0xf9, 0x79, 0xc5, 0xa9, 0x98, 0xb2, 0x46, 0x56, 0x5c, 0x3c, 0x1e, 0x20, 0x33, + 0x82, 0x53, 0x8b, 0xca, 0x32, 0x93, 0x53, 0x85, 0xb4, 0xb8, 0x58, 0xc1, 0x7c, 0x21, 0x3e, 0x3d, + 0xb0, 0xd9, 0x7a, 0x50, 0x83, 0xa5, 0xf8, 0xe1, 0x7c, 0x88, 0x59, 0x4a, 0x0c, 0x49, 0x6c, 0x60, + 0x47, 0x18, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x08, 0xad, 0x20, 0x98, 0x00, 0x00, 0x00, +} diff --git a/test/e2e/test_images/helloworld-grpc/proto/helloworld.proto b/test/e2e/test_images/helloworld-grpc/proto/helloworld.proto new file mode 100644 index 000000000000..ab5a9840865a --- /dev/null +++ b/test/e2e/test_images/helloworld-grpc/proto/helloworld.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package hello; + + +service HelloService { + rpc Hello(Request) returns (Response) {} +} + +message Request { + string msg = 1; +} + +message Response { + string msg = 1; +} + diff --git a/test/request.go b/test/request.go index 3d728257861d..ffd7a675a17d 100644 --- a/test/request.go +++ b/test/request.go @@ -31,8 +31,8 @@ import ( ) const ( - requestInterval = 1 * time.Second - requestTimeout = 1 * time.Minute + RequestInterval = 1 * time.Second + RequestTimeout = 1 * time.Minute ) func waitForRequestToDomainState(address string, spoofDomain string, retryableCodes []int, inState func(body string) (bool, error)) error { @@ -47,7 +47,7 @@ func waitForRequestToDomainState(address string, spoofDomain string, retryableCo } var body []byte - err = wait.PollImmediate(requestInterval, requestTimeout, func() (bool, error) { + err = wait.PollImmediate(RequestInterval, RequestTimeout, func() (bool, error) { resp, err := h.Do(req) if err != nil { return true, err @@ -98,3 +98,28 @@ func WaitForEndpointState(kubeClientset *kubernetes.Clientset, resolvableDomain // TODO(#348): The ingress endpoint tends to return 503's and 404's return waitForRequestToDomainState(endpoint, spoofDomain, []int{503, 404}, inState) } + +func FetchEndpointDomain(kubeClientset *kubernetes.Clientset, resolvableDomain bool, domain string, namespaceName string, routeName string) (string, string, error) { + var endpoint, spoofDomain string + + // If the domain that the Route controller is configured to assign to Route.Status.Domain + // (the domainSuffix) is not resolvable, we need to retrieve the IP of the endpoint and + // spoof the Host in our requests. + if !resolvableDomain { + ingressName := routeName + "-ela-ingress" + ingress, err := kubeClientset.ExtensionsV1beta1().Ingresses(namespaceName).Get(ingressName, metav1.GetOptions{}) + if err != nil { + return "", "", err + } + if ingress.Status.LoadBalancer.Ingress[0].IP == "" { + return "", "", fmt.Errorf("Expected ingress loadbalancer IP for %s to be set, instead was empty", ingressName) + } + endpoint = fmt.Sprintf("http://%s", ingress.Status.LoadBalancer.Ingress[0].IP) + spoofDomain = domain + } else { + // If the domain is resolvable, we can use it directly when we make requests + endpoint = domain + } + + return endpoint, spoofDomain, nil +}