diff --git a/components/proxy/conf/Caddyfile b/components/proxy/conf/Caddyfile index 9114498e4d473c..6cbe0c8b63c6ee 100644 --- a/components/proxy/conf/Caddyfile +++ b/components/proxy/conf/Caddyfile @@ -150,14 +150,7 @@ api.{$GITPOD_DOMAIN} { output stdout } - @grpc protocol grpc - - handle @grpc { - # gRPC traffic goes to gRPC server - reverse_proxy h2c://public-api-server.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:9001 - } - - # Non-grpc traffic goes to an HTTP server + # All traffic goes to HTTP endpoint. We handle gRPC using connect.build reverse_proxy public-api-server.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:9002 } diff --git a/components/public-api-server/pkg/apiv1/prebuild.go b/components/public-api-server/pkg/apiv1/prebuild.go index 973cf8803e4604..566a1c5f4d2f29 100644 --- a/components/public-api-server/pkg/apiv1/prebuild.go +++ b/components/public-api-server/pkg/apiv1/prebuild.go @@ -6,23 +6,24 @@ package apiv1 import ( "context" + + "github.com/bufbuild/connect-go" v1 "github.com/gitpod-io/gitpod/public-api/v1" + "github.com/gitpod-io/gitpod/public-api/v1/v1connect" ) func NewPrebuildService() *PrebuildService { - return &PrebuildService{ - UnimplementedPrebuildsServiceServer: &v1.UnimplementedPrebuildsServiceServer{}, - } + return &PrebuildService{} } type PrebuildService struct { - *v1.UnimplementedPrebuildsServiceServer + v1connect.UnimplementedPrebuildsServiceHandler } -func (p *PrebuildService) GetPrebuild(ctx context.Context, req *v1.GetPrebuildRequest) (*v1.GetPrebuildResponse, error) { - return &v1.GetPrebuildResponse{ +func (p *PrebuildService) GetPrebuild(ctx context.Context, req *connect.Request[v1.GetPrebuildRequest]) (*connect.Response[v1.GetPrebuildResponse], error) { + return connect.NewResponse(&v1.GetPrebuildResponse{ Prebuild: &v1.Prebuild{ - PrebuildId: req.GetPrebuildId(), + PrebuildId: req.Msg.GetPrebuildId(), Spec: &v1.PrebuildSpec{ Context: &v1.WorkspaceContext{ ContextUrl: "https://github.com/gitpod-io/gitpod", @@ -32,5 +33,5 @@ func (p *PrebuildService) GetPrebuild(ctx context.Context, req *v1.GetPrebuildRe }, Status: nil, }, - }, nil + }), nil } diff --git a/components/public-api-server/pkg/apiv1/prebuild_test.go b/components/public-api-server/pkg/apiv1/prebuild_test.go index dc014f61f697ca..5a9f6673329b12 100644 --- a/components/public-api-server/pkg/apiv1/prebuild_test.go +++ b/components/public-api-server/pkg/apiv1/prebuild_test.go @@ -6,18 +6,20 @@ package apiv1 import ( "context" + "testing" + + "github.com/bufbuild/connect-go" v1 "github.com/gitpod-io/gitpod/public-api/v1" "github.com/stretchr/testify/require" - "testing" ) func TestPrebuildService_GetPrebuild(t *testing.T) { svc := NewPrebuildService() prebuildID := "some-prebuild-id" - resp, err := svc.GetPrebuild(context.Background(), &v1.GetPrebuildRequest{ + resp, err := svc.GetPrebuild(context.Background(), connect.NewRequest(&v1.GetPrebuildRequest{ PrebuildId: prebuildID, - }) + })) require.NoError(t, err) require.Equal(t, &v1.GetPrebuildResponse{ Prebuild: &v1.Prebuild{ @@ -31,6 +33,6 @@ func TestPrebuildService_GetPrebuild(t *testing.T) { }, Status: nil, }, - }, resp) + }, resp.Msg) } diff --git a/components/public-api-server/pkg/apiv1/workspace.go b/components/public-api-server/pkg/apiv1/workspace.go index e202776729cfec..a095ac4756a42a 100644 --- a/components/public-api-server/pkg/apiv1/workspace.go +++ b/components/public-api-server/pkg/apiv1/workspace.go @@ -6,60 +6,48 @@ package apiv1 import ( "context" + "fmt" + connect "github.com/bufbuild/connect-go" protocol "github.com/gitpod-io/gitpod/gitpod-protocol" + "github.com/gitpod-io/gitpod/public-api-server/pkg/auth" "github.com/gitpod-io/gitpod/public-api-server/pkg/proxy" v1 "github.com/gitpod-io/gitpod/public-api/v1" + "github.com/gitpod-io/gitpod/public-api/v1/v1connect" "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" "github.com/relvacode/iso8601" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" ) func NewWorkspaceService(serverConnPool proxy.ServerConnectionPool) *WorkspaceService { return &WorkspaceService{ - connectionPool: serverConnPool, - UnimplementedWorkspacesServiceServer: &v1.UnimplementedWorkspacesServiceServer{}, + connectionPool: serverConnPool, } } type WorkspaceService struct { connectionPool proxy.ServerConnectionPool - *v1.UnimplementedWorkspacesServiceServer + v1connect.UnimplementedWorkspacesServiceHandler } -func (w *WorkspaceService) GetWorkspace(ctx context.Context, r *v1.GetWorkspaceRequest) (*v1.GetWorkspaceResponse, error) { +func (s *WorkspaceService) GetWorkspace(ctx context.Context, req *connect.Request[v1.GetWorkspaceRequest]) (*connect.Response[v1.GetWorkspaceResponse], error) { + token := auth.TokenFromContext(ctx) logger := ctxlogrus.Extract(ctx) - token, err := bearerTokenFromContext(ctx) - if err != nil { - return nil, err - } - server, err := w.connectionPool.Get(ctx, token) + server, err := s.connectionPool.Get(ctx, token) if err != nil { logger.WithError(err).Error("Failed to get connection to server.") - return nil, status.Error(codes.Internal, "failed to establish connection to downstream services") + return nil, connect.NewError(connect.CodeInternal, err) } - workspace, err := server.GetWorkspace(ctx, r.GetWorkspaceId()) + workspace, err := server.GetWorkspace(ctx, req.Msg.GetWorkspaceId()) if err != nil { logger.WithError(err).Error("Failed to get workspace.") - converted := proxy.ConvertError(err) - switch status.Code(converted) { - case codes.PermissionDenied: - return nil, status.Error(codes.PermissionDenied, "insufficient permission to access workspace") - case codes.NotFound: - return nil, status.Error(codes.NotFound, "workspace does not exist") - default: - return nil, status.Error(codes.Internal, "unable to retrieve workspace") - } + return nil, proxy.ConvertError(err) } - return &v1.GetWorkspaceResponse{ + return connect.NewResponse(&v1.GetWorkspaceResponse{ Result: &v1.Workspace{ WorkspaceId: workspace.Workspace.ID, OwnerId: workspace.Workspace.OwnerID, @@ -73,72 +61,40 @@ func (w *WorkspaceService) GetWorkspace(ctx context.Context, r *v1.GetWorkspaceR }, Description: workspace.Workspace.Description, }, - }, nil + }), nil } -func (w *WorkspaceService) GetOwnerToken(ctx context.Context, r *v1.GetOwnerTokenRequest) (*v1.GetOwnerTokenResponse, error) { +func (s *WorkspaceService) GetOwnerToken(ctx context.Context, req *connect.Request[v1.GetOwnerTokenRequest]) (*connect.Response[v1.GetOwnerTokenResponse], error) { logger := ctxlogrus.Extract(ctx) - token, err := bearerTokenFromContext(ctx) - if err != nil { - return nil, err - } + token := auth.TokenFromContext(ctx) - server, err := w.connectionPool.Get(ctx, token) + server, err := s.connectionPool.Get(ctx, token) if err != nil { logger.WithError(err).Error("Failed to get connection to server.") - return nil, status.Error(codes.Internal, "failed to establish connection to downstream services") + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to establish connection to downstream services")) } - ownerToken, err := server.GetOwnerToken(ctx, r.GetWorkspaceId()) + ownerToken, err := server.GetOwnerToken(ctx, req.Msg.GetWorkspaceId()) if err != nil { logger.WithError(err).Error("Failed to get owner token.") - converted := proxy.ConvertError(err) - switch status.Code(converted) { - case codes.PermissionDenied: - return nil, status.Error(codes.PermissionDenied, "insufficient permission to retrieve ownertoken") - case codes.NotFound: - return nil, status.Error(codes.NotFound, "workspace does not exist") - default: - return nil, status.Error(codes.Internal, "unable to retrieve owner token") - } - } - - return &v1.GetOwnerTokenResponse{Token: ownerToken}, nil -} - -func bearerTokenFromContext(ctx context.Context) (string, error) { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return "", status.Error(codes.Unauthenticated, "no credentials provided") - } - - values := md.Get("authorization") - if len(values) == 0 { - return "", status.Error(codes.Unauthenticated, "no authorization header specified") - } - if len(values) > 1 { - return "", status.Error(codes.Unauthenticated, "more than one authorization header specified, exactly one is required") + return nil, proxy.ConvertError(err) } - token := values[0] - return token, nil + return connect.NewResponse(&v1.GetOwnerTokenResponse{Token: ownerToken}), nil } -func (w *WorkspaceService) ListWorkspaces(ctx context.Context, req *v1.ListWorkspacesRequest) (*v1.ListWorkspacesResponse, error) { +func (s *WorkspaceService) ListWorkspaces(ctx context.Context, req *connect.Request[v1.ListWorkspacesRequest]) (*connect.Response[v1.ListWorkspacesResponse], error) { logger := ctxlogrus.Extract(ctx) - token, err := bearerTokenFromContext(ctx) - if err != nil { - return nil, err - } + token := auth.TokenFromContext(ctx) - server, err := w.connectionPool.Get(ctx, token) + server, err := s.connectionPool.Get(ctx, token) if err != nil { logger.WithError(err).Error("Failed to get connection to server.") - return nil, status.Error(codes.Internal, "failed to establish connection to downstream services") + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to establish connection to downstream services")) } - limit, err := getLimitFromPagination(req.Pagination) + limit, err := getLimitFromPagination(req.Msg.GetPagination()) if err != nil { // getLimitFromPagination returns gRPC errors return nil, err @@ -161,9 +117,11 @@ func (w *WorkspaceService) ListWorkspaces(ctx context.Context, req *v1.ListWorks res = append(res, workspaceAndInstance) } - return &v1.ListWorkspacesResponse{ - Result: res, - }, nil + return connect.NewResponse( + &v1.ListWorkspacesResponse{ + Result: res, + }, + ), nil } func getLimitFromPagination(pagination *v1.Pagination) (int, error) { @@ -179,7 +137,7 @@ func getLimitFromPagination(pagination *v1.Pagination) (int, error) { return defaultLimit, nil } if pagination.PageSize < 0 || maxLimit < pagination.PageSize { - return 0, grpc.Errorf(codes.InvalidArgument, "invalid pagination page size (must be 0 < x < %d)", maxLimit) + return 0, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid pagination page size (must be 0 < x < %d)", maxLimit)) } return int(pagination.PageSize), nil @@ -192,7 +150,7 @@ func convertWorkspaceInfo(input *protocol.WorkspaceInfo) (*v1.ListWorkspacesResp creationTime, err := parseGitpodTimestamp(wsi.CreationTime) if err != nil { // TODO(cw): should this really return an error and possibly fail the entire operation? - return nil, grpc.Errorf(codes.FailedPrecondition, "cannot parse creation time: %v", err) + return nil, connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("cannot parse creation time: %v", err)) } var phase v1.WorkspaceInstanceStatus_Phase @@ -219,7 +177,7 @@ func convertWorkspaceInfo(input *protocol.WorkspaceInfo) (*v1.ListWorkspacesResp phase = v1.WorkspaceInstanceStatus_PHASE_STOPPED default: // TODO(cw): should this really return an error and possibly fail the entire operation? - return nil, grpc.Errorf(codes.FailedPrecondition, "cannot convert instance phase: %s", wsi.Status.Phase) + return nil, connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("cannot convert instance phase: %s", wsi.Status.Phase)) } var admissionLevel v1.AdmissionLevel diff --git a/components/public-api-server/pkg/apiv1/workspace_test.go b/components/public-api-server/pkg/apiv1/workspace_test.go index dc7ed18a49bb75..5b29246b1e673a 100644 --- a/components/public-api-server/pkg/apiv1/workspace_test.go +++ b/components/public-api-server/pkg/apiv1/workspace_test.go @@ -7,21 +7,20 @@ package apiv1 import ( "context" "errors" + "net/http" "testing" "time" fuzz "github.com/AdaLogics/go-fuzz-headers" + "github.com/bufbuild/connect-go" "github.com/gitpod-io/gitpod/common-go/baseserver" protocol "github.com/gitpod-io/gitpod/gitpod-protocol" + "github.com/gitpod-io/gitpod/public-api-server/pkg/auth" v1 "github.com/gitpod-io/gitpod/public-api/v1" + "github.com/gitpod-io/gitpod/public-api/v1/v1connect" "github.com/golang/mock/gomock" "github.com/google/go-cmp/cmp" "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" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -33,21 +32,22 @@ func TestWorkspaceService_GetWorkspace(t *testing.T) { ) srv := baseserver.NewForTests(t, - baseserver.WithGRPC(baseserver.MustUseRandomLocalAddress(t)), + baseserver.WithHTTP(baseserver.MustUseRandomLocalAddress(t)), ) connPool := &FakeServerConnPool{} - v1.RegisterWorkspacesServiceServer(srv.GRPC(), NewWorkspaceService(connPool)) - baseserver.StartServerForTests(t, srv) - conn, err := grpc.Dial(srv.GRPCAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) + route, handler := v1connect.NewWorkspacesServiceHandler(NewWorkspaceService(connPool)) + srv.HTTPMux().Handle(route, handler) + + baseserver.StartServerForTests(t, srv) - client := v1.NewWorkspacesServiceClient(conn) - ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", bearerToken) + client := v1connect.NewWorkspacesServiceClient(http.DefaultClient, srv.HTTPAddress(), connect.WithInterceptors( + auth.NewClientInterceptor(bearerToken), + )) type Expectation struct { - Code codes.Code + Code connect.Code Response *v1.GetWorkspaceResponse } @@ -64,7 +64,6 @@ func TestWorkspaceService_GetWorkspace(t *testing.T) { foundWorkspaceID: workspaceTestData[0].Protocol, }, Expect: Expectation{ - Code: codes.OK, Response: &v1.GetWorkspaceResponse{ Result: workspaceTestData[0].API.Result, }, @@ -74,7 +73,7 @@ func TestWorkspaceService_GetWorkspace(t *testing.T) { name: "not found when workspace is not found by ID", WorkspaceID: "some-not-found-workspace-id", Expect: Expectation{ - Code: codes.NotFound, + Code: connect.CodeNotFound, }, }, } @@ -93,17 +92,16 @@ func TestWorkspaceService_GetWorkspace(t *testing.T) { }) connPool.api = srv - resp, err := client.GetWorkspace(ctx, &v1.GetWorkspaceRequest{ + resp, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{ WorkspaceId: test.WorkspaceID, - }) - if diff := cmp.Diff(test.Expect, Expectation{ - Code: status.Code(err), - Response: resp, - }, protocmp.Transform()); diff != "" { - t.Errorf("unexpected difference:\n%v", diff) + })) + requireErrorCode(t, test.Expect.Code, err) + if test.Expect.Response != nil { + if diff := cmp.Diff(test.Expect.Response, resp.Msg, protocmp.Transform()); diff != "" { + t.Errorf("unexpected difference:\n%v", diff) + } } }) - } } @@ -115,24 +113,24 @@ func TestWorkspaceService_GetOwnerToken(t *testing.T) { ) srv := baseserver.NewForTests(t, - baseserver.WithGRPC(baseserver.MustUseRandomLocalAddress(t)), + baseserver.WithHTTP(baseserver.MustUseRandomLocalAddress(t)), ) connPool := &FakeServerConnPool{} - v1.RegisterWorkspacesServiceServer(srv.GRPC(), NewWorkspaceService(connPool)) - baseserver.StartServerForTests(t, srv) - conn, err := grpc.Dial(srv.GRPCAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) + route, handler := v1connect.NewWorkspacesServiceHandler(NewWorkspaceService(connPool)) + srv.HTTPMux().Handle(route, handler) - client := v1.NewWorkspacesServiceClient(conn) - ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", bearerToken) + baseserver.StartServerForTests(t, srv) + + client := v1connect.NewWorkspacesServiceClient(http.DefaultClient, srv.HTTPAddress(), connect.WithInterceptors( + auth.NewClientInterceptor(bearerToken), + )) type Expectation struct { - Code codes.Code + Code connect.Code Response *v1.GetOwnerTokenResponse } - tests := []struct { name string WorkspaceID string @@ -144,7 +142,6 @@ func TestWorkspaceService_GetOwnerToken(t *testing.T) { WorkspaceID: foundWorkspaceID, Tokens: map[string]string{foundWorkspaceID: ownerToken}, Expect: Expectation{ - Code: codes.OK, Response: &v1.GetOwnerTokenResponse{ Token: ownerToken, }, @@ -154,7 +151,7 @@ func TestWorkspaceService_GetOwnerToken(t *testing.T) { name: "not found when workspace is not found by ID", WorkspaceID: "some-not-found-workspace-id", Expect: Expectation{ - Code: codes.NotFound, + Code: connect.CodeNotFound, }, }, } @@ -173,15 +170,14 @@ func TestWorkspaceService_GetOwnerToken(t *testing.T) { }) connPool.api = srv - resp, err := client.GetOwnerToken(ctx, &v1.GetOwnerTokenRequest{ + resp, err := client.GetOwnerToken(context.Background(), connect.NewRequest(&v1.GetOwnerTokenRequest{ WorkspaceId: test.WorkspaceID, - }) - act := Expectation{ - Code: status.Code(err), - Response: resp, - } - if diff := cmp.Diff(test.Expect, act, protocmp.Transform()); diff != "" { - t.Errorf("unexpected difference:\n%v", diff) + })) + requireErrorCode(t, test.Expect.Code, err) + if test.Expect.Response != nil { + if diff := cmp.Diff(test.Expect.Response, resp.Msg, protocmp.Transform()); diff != "" { + t.Errorf("unexpected difference:\n%v", diff) + } } }) } @@ -191,23 +187,25 @@ func TestWorkspaceService_ListWorkspaces(t *testing.T) { const ( bearerToken = "bearer-token-for-tests" ) + ctx := context.Background() srv := baseserver.NewForTests(t, - baseserver.WithGRPC(baseserver.MustUseRandomLocalAddress(t)), + baseserver.WithHTTP(baseserver.MustUseRandomLocalAddress(t)), ) connPool := &FakeServerConnPool{} - v1.RegisterWorkspacesServiceServer(srv.GRPC(), NewWorkspaceService(connPool)) - baseserver.StartServerForTests(t, srv) - conn, err := grpc.Dial(srv.GRPCAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) + route, handler := v1connect.NewWorkspacesServiceHandler(NewWorkspaceService(connPool)) + srv.HTTPMux().Handle(route, handler) + + baseserver.StartServerForTests(t, srv) - client := v1.NewWorkspacesServiceClient(conn) - ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", bearerToken) + client := v1connect.NewWorkspacesServiceClient(http.DefaultClient, srv.HTTPAddress(), connect.WithInterceptors( + auth.NewClientInterceptor(bearerToken), + )) type Expectation struct { - Code codes.Code + Code connect.Code Response *v1.ListWorkspacesResponse } @@ -222,7 +220,6 @@ func TestWorkspaceService_ListWorkspaces(t *testing.T) { Name: "empty list", Workspaces: []*protocol.WorkspaceInfo{}, Expectation: Expectation{ - Code: codes.OK, Response: &v1.ListWorkspacesResponse{}, }, }, @@ -232,7 +229,6 @@ func TestWorkspaceService_ListWorkspaces(t *testing.T) { &workspaceTestData[0].Protocol, }, Expectation: Expectation{ - Code: codes.OK, Response: &v1.ListWorkspacesResponse{ Result: []*v1.ListWorkspacesResponse_WorkspaceAndInstance{ &workspaceTestData[0].API, @@ -250,7 +246,7 @@ func TestWorkspaceService_ListWorkspaces(t *testing.T) { return []*protocol.WorkspaceInfo{&ws} }(), Expectation: Expectation{ - Code: codes.FailedPrecondition, + Code: connect.CodeFailedPrecondition, }, }, { @@ -266,7 +262,6 @@ func TestWorkspaceService_ListWorkspaces(t *testing.T) { }, PageSize: 42, Expectation: Expectation{ - Code: codes.OK, Response: &v1.ListWorkspacesResponse{}, }, }, @@ -274,7 +269,7 @@ func TestWorkspaceService_ListWorkspaces(t *testing.T) { Name: "excessive page size", PageSize: 1000, Expectation: Expectation{ - Code: codes.InvalidArgument, + Code: connect.CodeInvalidArgument, }, }, } @@ -296,17 +291,15 @@ func TestWorkspaceService_ListWorkspaces(t *testing.T) { } connPool.api = srv - resp, err := client.ListWorkspaces(ctx, &v1.ListWorkspacesRequest{ + resp, err := client.ListWorkspaces(ctx, connect.NewRequest(&v1.ListWorkspacesRequest{ Pagination: pagination, - }) - - act := Expectation{ - Code: status.Code(err), - Response: resp, - } + })) + requireErrorCode(t, test.Expectation.Code, err) - if diff := cmp.Diff(test.Expectation, act, protocmp.Transform()); diff != "" { - t.Errorf("unexpected difference:\n%v", diff) + if test.Expectation.Response != nil { + if diff := cmp.Diff(test.Expectation.Response, resp.Msg, protocmp.Transform()); diff != "" { + t.Errorf("unexpected difference:\n%v", diff) + } } }) } @@ -447,3 +440,13 @@ type FakeServerConnPool struct { func (f *FakeServerConnPool) Get(ctx context.Context, token string) (protocol.APIInterface, error) { return f.api, nil } + +func requireErrorCode(t *testing.T, expected connect.Code, err error) { + t.Helper() + if expected == 0 && err == nil { + return + } + + actual := connect.CodeOf(err) + require.Equal(t, expected, actual, "expected code %s, but got %s from error %v", expected.String(), actual.String(), err) +} diff --git a/components/public-api-server/pkg/proxy/errors.go b/components/public-api-server/pkg/proxy/errors.go index fa00dd1b8fa296..7d3db776aaf934 100644 --- a/components/public-api-server/pkg/proxy/errors.go +++ b/components/public-api-server/pkg/proxy/errors.go @@ -5,9 +5,9 @@ package proxy import ( - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "strings" + + "github.com/bufbuild/connect-go" ) func ConvertError(err error) error { @@ -18,23 +18,23 @@ func ConvertError(err error) error { s := err.Error() if strings.Contains(s, "code 401") { - return status.Error(codes.PermissionDenied, s) + return connect.NewError(connect.CodePermissionDenied, err) } // components/gitpod-protocol/src/messaging/error.ts if strings.Contains(s, "code 403") { - return status.Error(codes.PermissionDenied, s) + return connect.NewError(connect.CodePermissionDenied, err) } // components/gitpod-protocol/src/messaging/error.ts if strings.Contains(s, "code 404") { - return status.Error(codes.NotFound, s) + return connect.NewError(connect.CodeNotFound, err) } // components/gitpod-messagebus/src/jsonrpc-server.ts#47 if strings.Contains(s, "code -32603") { - return status.Error(codes.Internal, s) + return connect.NewError(connect.CodeInternal, err) } - return status.Error(codes.Internal, s) + return connect.NewError(connect.CodeInternal, err) } diff --git a/components/public-api-server/pkg/proxy/errors_test.go b/components/public-api-server/pkg/proxy/errors_test.go index da723bdda2b16c..8874450d88de0a 100644 --- a/components/public-api-server/pkg/proxy/errors_test.go +++ b/components/public-api-server/pkg/proxy/errors_test.go @@ -6,31 +6,32 @@ package proxy import ( "errors" - "github.com/stretchr/testify/require" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "fmt" "testing" + + "github.com/bufbuild/connect-go" + "github.com/stretchr/testify/require" ) func TestConvertError(t *testing.T) { scenarios := []struct { WebsocketError error - ExpectedStatus codes.Code + ExpectedStatus connect.Code }{ { WebsocketError: errors.New("reconnecting-ws: bad handshake: code 401 - URL: wss://main.preview.gitpod-dev.com/api/v1 - headers: map[Authorization:[Bearer foo] Origin:[http://main.preview.gitpod-dev.com/]]"), - ExpectedStatus: codes.PermissionDenied, + ExpectedStatus: connect.CodePermissionDenied, }, { WebsocketError: errors.New("jsonrpc2: code -32603 message: Request getWorkspace failed with message: No workspace with id 'some-id' found."), - ExpectedStatus: codes.Internal, + ExpectedStatus: connect.CodeInternal, }, } for _, s := range scenarios { converted := ConvertError(s.WebsocketError) - require.Equal(t, s.ExpectedStatus, status.Code(converted)) + require.Equal(t, s.ExpectedStatus, connect.CodeOf(converted)) // the error message should remain the same - require.Equal(t, s.WebsocketError.Error(), status.Convert(converted).Message()) + require.Equal(t, fmt.Errorf("%s: %w", s.ExpectedStatus.String(), s.WebsocketError).Error(), converted.Error()) } } diff --git a/components/public-api-server/pkg/server/integration_test.go b/components/public-api-server/pkg/server/integration_test.go index 26a5e7d0fb6dcd..4a3722d620a0b7 100644 --- a/components/public-api-server/pkg/server/integration_test.go +++ b/components/public-api-server/pkg/server/integration_test.go @@ -13,163 +13,111 @@ import ( "github.com/bufbuild/connect-go" "github.com/gitpod-io/gitpod/common-go/baseserver" "github.com/gitpod-io/gitpod/public-api-server/pkg/auth" + "github.com/gitpod-io/gitpod/public-api-server/pkg/proxy" v1 "github.com/gitpod-io/gitpod/public-api/v1" "github.com/gitpod-io/gitpod/public-api/v1/v1connect" "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" ) func TestPublicAPIServer_v1_WorkspaceService(t *testing.T) { - ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "some-token") + ctx := context.Background() srv := baseserver.NewForTests(t, - baseserver.WithGRPC(baseserver.MustUseRandomLocalAddress(t)), + baseserver.WithHTTP(baseserver.MustUseRandomLocalAddress(t)), ) gitpodAPI, err := url.Parse("wss://main.preview.gitpod-dev.com/api/v1") require.NoError(t, err) - require.NoError(t, register(srv, gitpodAPI)) - baseserver.StartServerForTests(t, srv) + connPool := &proxy.NoConnectionPool{ServerAPI: gitpodAPI} - conn, err := grpc.Dial(srv.GRPCAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) + require.NoError(t, register(srv, connPool)) + baseserver.StartServerForTests(t, srv) - workspaceClient := v1.NewWorkspacesServiceClient(conn) + workspaceClient := v1connect.NewWorkspacesServiceClient(http.DefaultClient, srv.HTTPAddress(), connect.WithInterceptors(auth.NewClientInterceptor("some-token"))) - _, err = workspaceClient.CreateAndStartWorkspace(ctx, &v1.CreateAndStartWorkspaceRequest{}) - requireErrorStatusCode(t, codes.Unimplemented, err) + _, err = workspaceClient.CreateAndStartWorkspace(ctx, connect.NewRequest(&v1.CreateAndStartWorkspaceRequest{})) + requireErrorStatusCode(t, connect.CodeUnimplemented, err) - _, err = workspaceClient.StartWorkspace(ctx, &v1.StartWorkspaceRequest{}) - requireErrorStatusCode(t, codes.Unimplemented, err) + _, err = workspaceClient.StartWorkspace(ctx, connect.NewRequest(&v1.StartWorkspaceRequest{})) + requireErrorStatusCode(t, connect.CodeUnimplemented, err) - _, err = workspaceClient.GetActiveWorkspaceInstance(ctx, &v1.GetActiveWorkspaceInstanceRequest{}) - requireErrorStatusCode(t, codes.Unimplemented, err) + _, err = workspaceClient.GetActiveWorkspaceInstance(ctx, connect.NewRequest(&v1.GetActiveWorkspaceInstanceRequest{})) + requireErrorStatusCode(t, connect.CodeUnimplemented, err) - _, err = workspaceClient.GetWorkspaceInstanceOwnerToken(ctx, &v1.GetWorkspaceInstanceOwnerTokenRequest{}) - requireErrorStatusCode(t, codes.Unimplemented, err) + _, err = workspaceClient.GetWorkspaceInstanceOwnerToken(ctx, connect.NewRequest(&v1.GetWorkspaceInstanceOwnerTokenRequest{})) + requireErrorStatusCode(t, connect.CodeUnimplemented, err) - stopWorkspaceStream, err := workspaceClient.StopWorkspace(ctx, &v1.StopWorkspaceRequest{}) + stopWorkspaceStream, err := workspaceClient.StopWorkspace(ctx, connect.NewRequest(&v1.StopWorkspaceRequest{})) require.NoError(t, err) - _, err = stopWorkspaceStream.Recv() - requireErrorStatusCode(t, codes.Unimplemented, err) + _ = stopWorkspaceStream.Receive() + err = stopWorkspaceStream.Err() + requireErrorStatusCode(t, connect.CodeUnimplemented, err) - listenWorkspaceStream, err := workspaceClient.ListenToWorkspaceInstance(ctx, &v1.ListenToWorkspaceInstanceRequest{}) + listenWorkspaceStream, err := workspaceClient.ListenToWorkspaceInstance(ctx, connect.NewRequest(&v1.ListenToWorkspaceInstanceRequest{})) require.NoError(t, err) - _, err = listenWorkspaceStream.Recv() - requireErrorStatusCode(t, codes.Unimplemented, err) + _ = listenWorkspaceStream.Receive() + err = listenWorkspaceStream.Err() + requireErrorStatusCode(t, connect.CodeUnimplemented, err) - listenImageBuildStream, err := workspaceClient.ListenToImageBuildLogs(ctx, &v1.ListenToImageBuildLogsRequest{}) + listenImageBuildStream, err := workspaceClient.ListenToImageBuildLogs(ctx, connect.NewRequest(&v1.ListenToImageBuildLogsRequest{})) require.NoError(t, err) - _, err = listenImageBuildStream.Recv() - requireErrorStatusCode(t, codes.Unimplemented, err) + _ = listenImageBuildStream.Receive() + err = listenImageBuildStream.Err() + requireErrorStatusCode(t, connect.CodeUnimplemented, err) } func TestPublicAPIServer_v1_PrebuildService(t *testing.T) { ctx := context.Background() - srv := baseserver.NewForTests(t, baseserver.WithGRPC(baseserver.MustUseRandomLocalAddress(t))) + srv := baseserver.NewForTests(t, baseserver.WithHTTP(baseserver.MustUseRandomLocalAddress(t))) gitpodAPI, err := url.Parse("wss://main.preview.gitpod-dev.com/api/v1") require.NoError(t, err) - require.NoError(t, register(srv, gitpodAPI)) - - baseserver.StartServerForTests(t, srv) - - conn, err := grpc.Dial(srv.GRPCAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) - - prebuildClient := v1.NewPrebuildsServiceClient(conn) + connPool := &proxy.NoConnectionPool{ServerAPI: gitpodAPI} - _, err = prebuildClient.GetPrebuild(ctx, &v1.GetPrebuildRequest{}) - requireErrorStatusCode(t, codes.Unimplemented, err) + require.NoError(t, register(srv, connPool)) - _, err = prebuildClient.GetRunningPrebuild(ctx, &v1.GetRunningPrebuildRequest{}) - requireErrorStatusCode(t, codes.Unimplemented, err) - - listenToStatusStream, err := prebuildClient.ListenToPrebuildStatus(ctx, &v1.ListenToPrebuildStatusRequest{}) - require.NoError(t, err) - _, err = listenToStatusStream.Recv() - requireErrorStatusCode(t, codes.Unimplemented, err) - - listenToLogsStream, err := prebuildClient.ListenToPrebuildLogs(ctx, &v1.ListenToPrebuildLogsRequest{}) - require.NoError(t, err) - _, err = listenToLogsStream.Recv() - requireErrorStatusCode(t, codes.Unimplemented, err) -} - -func TestPublicAPIServer_WorkspaceServiceHandler(t *testing.T) { - ctx := context.Background() - srv := baseserver.NewForTests(t, - baseserver.WithGRPC(baseserver.MustUseRandomLocalAddress(t)), - baseserver.WithHTTP(baseserver.MustUseRandomLocalAddress(t)), - ) - - gitpodAPI, err := url.Parse("wss://main.preview.gitpod-dev.com/api/v1") - require.NoError(t, err) - - require.NoError(t, register(srv, gitpodAPI)) baseserver.StartServerForTests(t, srv) - client := v1connect.NewWorkspacesServiceClient(http.DefaultClient, srv.HTTPAddress(), connect.WithInterceptors(auth.NewClientInterceptor("token"))) - - _, err = client.ListWorkspaces(ctx, connect.NewRequest(&v1.ListWorkspacesRequest{})) - require.Equal(t, connect.CodeUnimplemented.String(), connect.CodeOf(err).String()) - - _, err = client.GetWorkspace(ctx, connect.NewRequest(&v1.GetWorkspaceRequest{})) - require.Equal(t, connect.CodeUnimplemented.String(), connect.CodeOf(err).String()) - - _, err = client.GetOwnerToken(ctx, connect.NewRequest(&v1.GetOwnerTokenRequest{})) - require.Equal(t, connect.CodeUnimplemented.String(), connect.CodeOf(err).String()) - - _, err = client.CreateAndStartWorkspace(ctx, connect.NewRequest(&v1.CreateAndStartWorkspaceRequest{})) - require.Equal(t, connect.CodeUnimplemented.String(), connect.CodeOf(err).String()) + prebuildClient := v1connect.NewPrebuildsServiceClient(http.DefaultClient, srv.HTTPAddress(), connect.WithInterceptors(auth.NewClientInterceptor("some-token"))) - _, err = client.StartWorkspace(ctx, connect.NewRequest(&v1.StartWorkspaceRequest{})) - require.Equal(t, connect.CodeUnimplemented.String(), connect.CodeOf(err).String()) + _, err = prebuildClient.GetRunningPrebuild(ctx, connect.NewRequest(&v1.GetRunningPrebuildRequest{})) + requireErrorStatusCode(t, connect.CodeUnimplemented, err) - _, err = client.GetActiveWorkspaceInstance(ctx, connect.NewRequest(&v1.GetActiveWorkspaceInstanceRequest{})) - require.Equal(t, connect.CodeUnimplemented.String(), connect.CodeOf(err).String()) - - _, err = client.GetWorkspaceInstanceOwnerToken(ctx, connect.NewRequest(&v1.GetWorkspaceInstanceOwnerTokenRequest{})) - require.Equal(t, connect.CodeUnimplemented.String(), connect.CodeOf(err).String()) - - stream, err := client.ListenToWorkspaceInstance(ctx, connect.NewRequest(&v1.ListenToWorkspaceInstanceRequest{})) + listenToStatusStream, err := prebuildClient.ListenToPrebuildStatus(ctx, connect.NewRequest(&v1.ListenToPrebuildStatusRequest{})) require.NoError(t, err) - stream.Receive() - require.Equal(t, connect.CodeUnimplemented.String(), connect.CodeOf(stream.Err()).String()) + _ = listenToStatusStream.Receive() + err = listenToStatusStream.Err() + requireErrorStatusCode(t, connect.CodeUnimplemented, err) - logsStream, err := client.ListenToImageBuildLogs(ctx, connect.NewRequest(&v1.ListenToImageBuildLogsRequest{})) + listenToLogsStream, err := prebuildClient.ListenToPrebuildLogs(ctx, connect.NewRequest(&v1.ListenToPrebuildLogsRequest{})) require.NoError(t, err) - logsStream.Receive() - require.Equal(t, connect.CodeUnimplemented.String(), connect.CodeOf(logsStream.Err()).String()) - - stopStream, err := client.StopWorkspace(ctx, connect.NewRequest(&v1.StopWorkspaceRequest{})) - require.NoError(t, err) - stopStream.Receive() - require.Equal(t, connect.CodeUnimplemented.String(), connect.CodeOf(stopStream.Err()).String()) + _ = listenToLogsStream.Receive() + err = listenToLogsStream.Err() + requireErrorStatusCode(t, connect.CodeUnimplemented, err) } -func requireErrorStatusCode(t *testing.T, expected codes.Code, err error) { - require.Error(t, err) - st, ok := status.FromError(err) - require.True(t, ok) - require.Equalf(t, expected, st.Code(), "expected: %s but got: %s", expected.String(), st.String()) +func requireErrorStatusCode(t *testing.T, expected connect.Code, err error) { + t.Helper() + if expected == 0 && err == nil { + return + } + + actual := connect.CodeOf(err) + require.Equal(t, expected, actual, "expected code %s, but got %s from error %v", expected.String(), actual.String(), err) } func TestConnectWorkspaceService_RequiresAuth(t *testing.T) { srv := baseserver.NewForTests(t, baseserver.WithHTTP(baseserver.MustUseRandomLocalAddress(t)), - baseserver.WithGRPC(baseserver.MustUseRandomLocalAddress(t)), ) gitpodAPI, err := url.Parse("wss://main.preview.gitpod-dev.com/api/v1") require.NoError(t, err) - require.NoError(t, register(srv, gitpodAPI)) + connPool := &proxy.NoConnectionPool{ServerAPI: gitpodAPI} + + require.NoError(t, register(srv, connPool)) baseserver.StartServerForTests(t, srv) @@ -177,19 +125,19 @@ func TestConnectWorkspaceService_RequiresAuth(t *testing.T) { _, err = clientWithoutAuth.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{WorkspaceId: "123"})) require.Error(t, err) require.Equal(t, connect.CodeUnauthenticated, connect.CodeOf(err)) - } func TestConnectPrebuildsService_RequiresAuth(t *testing.T) { srv := baseserver.NewForTests(t, baseserver.WithHTTP(baseserver.MustUseRandomLocalAddress(t)), - baseserver.WithGRPC(baseserver.MustUseRandomLocalAddress(t)), ) gitpodAPI, err := url.Parse("wss://main.preview.gitpod-dev.com/api/v1") require.NoError(t, err) - require.NoError(t, register(srv, gitpodAPI)) + connPool := &proxy.NoConnectionPool{ServerAPI: gitpodAPI} + + require.NoError(t, register(srv, connPool)) baseserver.StartServerForTests(t, srv) diff --git a/components/public-api-server/pkg/server/server.go b/components/public-api-server/pkg/server/server.go index fb02131b2dab00..d57a614b7c6880 100644 --- a/components/public-api-server/pkg/server/server.go +++ b/components/public-api-server/pkg/server/server.go @@ -24,7 +24,6 @@ import ( "github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice" "github.com/gitpod-io/gitpod/public-api-server/pkg/proxy" "github.com/gitpod-io/gitpod/public-api-server/pkg/webhooks" - v1 "github.com/gitpod-io/gitpod/public-api/v1" "github.com/sirupsen/logrus" ) @@ -36,6 +35,8 @@ func Start(logger *logrus.Entry, version string, cfg *config.Configuration) erro return fmt.Errorf("failed to parse Gitpod API URL: %w", err) } + connPool := &proxy.NoConnectionPool{ServerAPI: gitpodAPI} + srv, err := baseserver.New("public_api_server", baseserver.WithLogger(logger), baseserver.WithConfig(cfg.Server), @@ -66,7 +67,7 @@ func Start(logger *logrus.Entry, version string, cfg *config.Configuration) erro srv.HTTPMux().Handle("/stripe/invoices/webhook", handlers.ContentTypeHandler(stripeWebhookHandler, "application/json")) - if registerErr := register(srv, gitpodAPI); registerErr != nil { + if registerErr := register(srv, connPool); registerErr != nil { return fmt.Errorf("failed to register services: %w", registerErr) } @@ -77,24 +78,19 @@ func Start(logger *logrus.Entry, version string, cfg *config.Configuration) erro return nil } -func register(srv *baseserver.Server, serverAPIURL *url.URL) error { +func register(srv *baseserver.Server, connPool proxy.ServerConnectionPool) error { proxy.RegisterMetrics(srv.MetricsRegistry()) - connPool := &proxy.NoConnectionPool{ServerAPI: serverAPIURL} - - v1.RegisterWorkspacesServiceServer(srv.GRPC(), apiv1.NewWorkspaceService(connPool)) - v1.RegisterPrebuildsServiceServer(srv.GRPC(), v1.UnimplementedPrebuildsServiceServer{}) - handlerOptions := []connect.HandlerOption{ connect.WithInterceptors( auth.NewServerInterceptor(), ), } - workspacesRoute, workspacesServiceHandler := v1connect.NewWorkspacesServiceHandler(&v1connect.UnimplementedWorkspacesServiceHandler{}, handlerOptions...) + workspacesRoute, workspacesServiceHandler := v1connect.NewWorkspacesServiceHandler(apiv1.NewWorkspaceService(connPool), handlerOptions...) srv.HTTPMux().Handle(workspacesRoute, workspacesServiceHandler) - prebuildsRoute, prebuildsServiceHandler := v1connect.NewPrebuildsServiceHandler(&v1connect.UnimplementedPrebuildsServiceHandler{}, handlerOptions...) + prebuildsRoute, prebuildsServiceHandler := v1connect.NewPrebuildsServiceHandler(apiv1.NewPrebuildService(), handlerOptions...) srv.HTTPMux().Handle(prebuildsRoute, prebuildsServiceHandler) return nil diff --git a/dev/gpctl/cmd/public-api.go b/dev/gpctl/cmd/public-api.go index 3cbf35da9235e9..8cdc3835ea5e4d 100644 --- a/dev/gpctl/cmd/public-api.go +++ b/dev/gpctl/cmd/public-api.go @@ -51,7 +51,7 @@ func newPublicAPIConn() (*grpc.ClientConn, error) { opts := []grpc.DialOption{ // attach token to requests to auth grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - withAuth := metadata.AppendToOutgoingContext(ctx, "authorization", publicApiCmdOpts.token) + withAuth := metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+publicApiCmdOpts.token) return invoker(withAuth, method, req, reply, cc, opts...) }), }