From 9ec29612ea418ba3477d58d864d248a0dfd0591f Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Mon, 16 Mar 2026 23:26:58 +0800 Subject: [PATCH] feat(connectrpc): integrate connect-go HealthService as proof-of-concept - Add protobuf definition for HealthService with Check RPC - Add buf v2 configuration for codegen with connect-go and protobuf plugins - Implement connect-go HealthServer that delegates to store.Health() - Create composite HTTP handler using http.ServeMux for path-prefix routing - Wire composite handler into bootstrap so connect-go and Gin share the same port - Add proto and proto-lint Makefile targets and include proto in generate chain - Add connectrpc.com/connect dependency - Add test for HealthServer Check endpoint Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 14 +- buf.gen.yaml | 8 + buf.yaml | 9 + go.mod | 3 +- go.sum | 2 + internal/bootstrap/bootstrap.go | 5 +- internal/bootstrap/server.go | 18 ++ internal/connectrpc/health.go | 40 +++ internal/connectrpc/health_test.go | 38 +++ internal/gen/health/v1/health.pb.go | 230 ++++++++++++++++++ .../v1/healthv1connect/health.connect.go | 108 ++++++++ proto/health/v1/health.proto | 22 ++ 12 files changed, 491 insertions(+), 6 deletions(-) create mode 100644 buf.gen.yaml create mode 100644 buf.yaml create mode 100644 internal/connectrpc/health.go create mode 100644 internal/connectrpc/health_test.go create mode 100644 internal/gen/health/v1/health.pb.go create mode 100644 internal/gen/health/v1/healthv1connect/health.connect.go create mode 100644 proto/health/v1/health.proto diff --git a/Makefile b/Makefile index 7da045a6..44509040 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,7 @@ assets-dev: ## clean: remove build artifacts and test coverage clean: - rm -rf bin/ release/ coverage.txt internal/templates/static/dist/ + rm -rf bin/ release/ coverage.txt internal/templates/static/dist/ internal/gen/ find internal/templates -name "*_templ.go" -delete rm -f internal/templates/asset_paths.go @@ -101,14 +101,22 @@ rebuild: clean build .PHONY: build_all_linux_amd64 build_all_linux_arm64 generate watch air dev mocks .PHONY: install-tools mod-download mod-tidy mod-verify check-tools version .PHONY: docker-build docker-run swagger swagger-init swagger-fmt swagger-validate -.PHONY: assets assets-dev +.PHONY: assets assets-dev proto proto-lint ## install-tools: download tool dependencies install-tools: $(GO) mod download $(TOOLS_MOD) +## proto: generate protobuf and connect-go code +proto: + buf generate + +## proto-lint: lint protobuf definitions +proto-lint: + buf lint + ## generate: run go generate (templ compilation + mocks via go:generate directives) -generate: install-tools assets swagger +generate: install-tools assets swagger proto $(GO) generate ./... ## mocks: generate mock files only (all directives in internal/mocks/) diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 00000000..3dc61c08 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,8 @@ +version: v2 +plugins: + - remote: buf.build/protocolbuffers/go + out: internal/gen + opt: paths=source_relative + - remote: buf.build/connectrpc/go + out: internal/gen + opt: paths=source_relative diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 00000000..c7e30e38 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: proto +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/go.mod b/go.mod index 8cebe22b..368fb07e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/go-authgate/authgate go 1.25.0 require ( + connectrpc.com/connect v1.19.1 github.com/a-h/templ v0.3.1001 github.com/appleboy/com v1.2.0 github.com/appleboy/go-httpclient v0.10.0 @@ -29,6 +30,7 @@ require ( go.uber.org/mock v0.6.0 golang.org/x/crypto v0.49.0 golang.org/x/oauth2 v0.36.0 + google.golang.org/protobuf v1.36.11 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 @@ -147,6 +149,5 @@ require ( golang.org/x/text v0.35.0 // indirect golang.org/x/tools v0.42.0 // indirect google.golang.org/grpc v1.79.1 // indirect - google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 209e8907..b861945d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= +connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index b803a1f0..0ebb66cd 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -172,8 +172,9 @@ func (app *Application) initializeHTTPLayer() { oauthProviders, ) - // HTTP Server - app.Server = createHTTPServer(app.Config, app.Router) + // HTTP Server (composite handler routes connect-go services + Gin) + compositeHandler := createCompositeHandler(app.DB, app.Router) + app.Server = createHTTPServer(app.Config, compositeHandler) } // startWithGracefulShutdown starts the server and handles graceful shutdown diff --git a/internal/bootstrap/server.go b/internal/bootstrap/server.go index 84328b2e..49824e81 100644 --- a/internal/bootstrap/server.go +++ b/internal/bootstrap/server.go @@ -7,7 +7,9 @@ import ( "time" "github.com/go-authgate/authgate/internal/config" + authconnect "github.com/go-authgate/authgate/internal/connectrpc" "github.com/go-authgate/authgate/internal/core" + "github.com/go-authgate/authgate/internal/gen/health/v1/healthv1connect" "github.com/go-authgate/authgate/internal/metrics" "github.com/go-authgate/authgate/internal/models" "github.com/go-authgate/authgate/internal/services" @@ -29,6 +31,22 @@ func createHTTPServer(cfg *config.Config, handler http.Handler) *http.Server { } } +// createCompositeHandler creates an http.Handler that routes connect-go services +// by path prefix, falling back to the Gin router for everything else. +func createCompositeHandler(db *store.Store, ginRouter http.Handler) http.Handler { + mux := http.NewServeMux() + + // Register connect-go HealthService + healthServer := authconnect.NewHealthServer(db) + path, handler := healthv1connect.NewHealthServiceHandler(healthServer) + mux.Handle(path, handler) + + // Fall back to Gin for all other routes + mux.Handle("/", ginRouter) + + return mux +} + // addServerRunningJob adds the HTTP server running job func addServerRunningJob(m *graceful.Manager, srv *http.Server) { m.AddRunningJob(func(ctx context.Context) error { diff --git a/internal/connectrpc/health.go b/internal/connectrpc/health.go new file mode 100644 index 00000000..b887730b --- /dev/null +++ b/internal/connectrpc/health.go @@ -0,0 +1,40 @@ +package connectrpc + +import ( + "context" + + "connectrpc.com/connect" + + healthv1 "github.com/go-authgate/authgate/internal/gen/health/v1" + "github.com/go-authgate/authgate/internal/gen/health/v1/healthv1connect" + "github.com/go-authgate/authgate/internal/store" +) + +// HealthServer implements the connect-go HealthService. +type HealthServer struct { + healthv1connect.UnimplementedHealthServiceHandler + db *store.Store +} + +// NewHealthServer creates a new HealthServer. +func NewHealthServer(db *store.Store) *HealthServer { + return &HealthServer{db: db} +} + +// Check returns the health status of the service. +func (s *HealthServer) Check( + _ context.Context, + _ *connect.Request[healthv1.HealthCheckRequest], +) (*connect.Response[healthv1.HealthCheckResponse], error) { + resp := &healthv1.HealthCheckResponse{} + + if err := s.db.Health(); err != nil { + resp.Status = healthv1.HealthCheckResponse_SERVING_STATUS_NOT_SERVING + resp.Database = "disconnected" + } else { + resp.Status = healthv1.HealthCheckResponse_SERVING_STATUS_SERVING + resp.Database = "connected" + } + + return connect.NewResponse(resp), nil +} diff --git a/internal/connectrpc/health_test.go b/internal/connectrpc/health_test.go new file mode 100644 index 00000000..ffb68f8b --- /dev/null +++ b/internal/connectrpc/health_test.go @@ -0,0 +1,38 @@ +package connectrpc + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/go-authgate/authgate/internal/config" + healthv1 "github.com/go-authgate/authgate/internal/gen/health/v1" + "github.com/go-authgate/authgate/internal/gen/health/v1/healthv1connect" + "github.com/go-authgate/authgate/internal/store" +) + +func TestHealthServer_Check_Serving(t *testing.T) { + db, err := store.New(context.Background(), "sqlite", ":memory:", &config.Config{}) + require.NoError(t, err) + + mux := http.NewServeMux() + path, handler := healthv1connect.NewHealthServiceHandler(NewHealthServer(db)) + mux.Handle(path, handler) + + srv := httptest.NewServer(mux) + defer srv.Close() + + client := healthv1connect.NewHealthServiceClient(srv.Client(), srv.URL) + resp, err := client.Check( + context.Background(), + connect.NewRequest(&healthv1.HealthCheckRequest{}), + ) + require.NoError(t, err) + assert.Equal(t, healthv1.HealthCheckResponse_SERVING_STATUS_SERVING, resp.Msg.Status) + assert.Equal(t, "connected", resp.Msg.Database) +} diff --git a/internal/gen/health/v1/health.pb.go b/internal/gen/health/v1/health.pb.go new file mode 100644 index 00000000..1bc96fe4 --- /dev/null +++ b/internal/gen/health/v1/health.pb.go @@ -0,0 +1,230 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: health/v1/health.proto + +package healthv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +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 HealthCheckResponse_ServingStatus int32 + +const ( + HealthCheckResponse_SERVING_STATUS_UNSPECIFIED HealthCheckResponse_ServingStatus = 0 + HealthCheckResponse_SERVING_STATUS_SERVING HealthCheckResponse_ServingStatus = 1 + HealthCheckResponse_SERVING_STATUS_NOT_SERVING HealthCheckResponse_ServingStatus = 2 +) + +// Enum value maps for HealthCheckResponse_ServingStatus. +var ( + HealthCheckResponse_ServingStatus_name = map[int32]string{ + 0: "SERVING_STATUS_UNSPECIFIED", + 1: "SERVING_STATUS_SERVING", + 2: "SERVING_STATUS_NOT_SERVING", + } + HealthCheckResponse_ServingStatus_value = map[string]int32{ + "SERVING_STATUS_UNSPECIFIED": 0, + "SERVING_STATUS_SERVING": 1, + "SERVING_STATUS_NOT_SERVING": 2, + } +) + +func (x HealthCheckResponse_ServingStatus) Enum() *HealthCheckResponse_ServingStatus { + p := new(HealthCheckResponse_ServingStatus) + *p = x + return p +} + +func (x HealthCheckResponse_ServingStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor { + return file_health_v1_health_proto_enumTypes[0].Descriptor() +} + +func (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType { + return &file_health_v1_health_proto_enumTypes[0] +} + +func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead. +func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) { + return file_health_v1_health_proto_rawDescGZIP(), []int{1, 0} +} + +type HealthCheckRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthCheckRequest) Reset() { + *x = HealthCheckRequest{} + mi := &file_health_v1_health_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthCheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckRequest) ProtoMessage() {} + +func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_health_v1_health_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead. +func (*HealthCheckRequest) Descriptor() ([]byte, []int) { + return file_health_v1_health_proto_rawDescGZIP(), []int{0} +} + +type HealthCheckResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status HealthCheckResponse_ServingStatus `protobuf:"varint,1,opt,name=status,proto3,enum=health.v1.HealthCheckResponse_ServingStatus" json:"status,omitempty"` + Database string `protobuf:"bytes,2,opt,name=database,proto3" json:"database,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthCheckResponse) Reset() { + *x = HealthCheckResponse{} + mi := &file_health_v1_health_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthCheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckResponse) ProtoMessage() {} + +func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_health_v1_health_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead. +func (*HealthCheckResponse) Descriptor() ([]byte, []int) { + return file_health_v1_health_proto_rawDescGZIP(), []int{1} +} + +func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus { + if x != nil { + return x.Status + } + return HealthCheckResponse_SERVING_STATUS_UNSPECIFIED +} + +func (x *HealthCheckResponse) GetDatabase() string { + if x != nil { + return x.Database + } + return "" +} + +var File_health_v1_health_proto protoreflect.FileDescriptor + +const file_health_v1_health_proto_rawDesc = "" + + "\n" + + "\x16health/v1/health.proto\x12\thealth.v1\"\x14\n" + + "\x12HealthCheckRequest\"\xe4\x01\n" + + "\x13HealthCheckResponse\x12D\n" + + "\x06status\x18\x01 \x01(\x0e2,.health.v1.HealthCheckResponse.ServingStatusR\x06status\x12\x1a\n" + + "\bdatabase\x18\x02 \x01(\tR\bdatabase\"k\n" + + "\rServingStatus\x12\x1e\n" + + "\x1aSERVING_STATUS_UNSPECIFIED\x10\x00\x12\x1a\n" + + "\x16SERVING_STATUS_SERVING\x10\x01\x12\x1e\n" + + "\x1aSERVING_STATUS_NOT_SERVING\x10\x022Y\n" + + "\rHealthService\x12H\n" + + "\x05Check\x12\x1d.health.v1.HealthCheckRequest\x1a\x1e.health.v1.HealthCheckResponse\"\x00BAZ?github.com/go-authgate/authgate/internal/gen/health/v1;healthv1b\x06proto3" + +var ( + file_health_v1_health_proto_rawDescOnce sync.Once + file_health_v1_health_proto_rawDescData []byte +) + +func file_health_v1_health_proto_rawDescGZIP() []byte { + file_health_v1_health_proto_rawDescOnce.Do(func() { + file_health_v1_health_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_health_v1_health_proto_rawDesc), len(file_health_v1_health_proto_rawDesc))) + }) + return file_health_v1_health_proto_rawDescData +} + +var file_health_v1_health_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_health_v1_health_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_health_v1_health_proto_goTypes = []any{ + (HealthCheckResponse_ServingStatus)(0), // 0: health.v1.HealthCheckResponse.ServingStatus + (*HealthCheckRequest)(nil), // 1: health.v1.HealthCheckRequest + (*HealthCheckResponse)(nil), // 2: health.v1.HealthCheckResponse +} +var file_health_v1_health_proto_depIdxs = []int32{ + 0, // 0: health.v1.HealthCheckResponse.status:type_name -> health.v1.HealthCheckResponse.ServingStatus + 1, // 1: health.v1.HealthService.Check:input_type -> health.v1.HealthCheckRequest + 2, // 2: health.v1.HealthService.Check:output_type -> health.v1.HealthCheckResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_health_v1_health_proto_init() } +func file_health_v1_health_proto_init() { + if File_health_v1_health_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_health_v1_health_proto_rawDesc), len(file_health_v1_health_proto_rawDesc)), + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_health_v1_health_proto_goTypes, + DependencyIndexes: file_health_v1_health_proto_depIdxs, + EnumInfos: file_health_v1_health_proto_enumTypes, + MessageInfos: file_health_v1_health_proto_msgTypes, + }.Build() + File_health_v1_health_proto = out.File + file_health_v1_health_proto_goTypes = nil + file_health_v1_health_proto_depIdxs = nil +} diff --git a/internal/gen/health/v1/healthv1connect/health.connect.go b/internal/gen/health/v1/healthv1connect/health.connect.go new file mode 100644 index 00000000..d35d474d --- /dev/null +++ b/internal/gen/health/v1/healthv1connect/health.connect.go @@ -0,0 +1,108 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: health/v1/health.proto + +package healthv1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/go-authgate/authgate/internal/gen/health/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // HealthServiceName is the fully-qualified name of the HealthService service. + HealthServiceName = "health.v1.HealthService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // HealthServiceCheckProcedure is the fully-qualified name of the HealthService's Check RPC. + HealthServiceCheckProcedure = "/health.v1.HealthService/Check" +) + +// HealthServiceClient is a client for the health.v1.HealthService service. +type HealthServiceClient interface { + Check(context.Context, *connect.Request[v1.HealthCheckRequest]) (*connect.Response[v1.HealthCheckResponse], error) +} + +// NewHealthServiceClient constructs a client for the health.v1.HealthService service. By default, +// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and +// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() +// or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewHealthServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) HealthServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + healthServiceMethods := v1.File_health_v1_health_proto.Services().ByName("HealthService").Methods() + return &healthServiceClient{ + check: connect.NewClient[v1.HealthCheckRequest, v1.HealthCheckResponse]( + httpClient, + baseURL+HealthServiceCheckProcedure, + connect.WithSchema(healthServiceMethods.ByName("Check")), + connect.WithClientOptions(opts...), + ), + } +} + +// healthServiceClient implements HealthServiceClient. +type healthServiceClient struct { + check *connect.Client[v1.HealthCheckRequest, v1.HealthCheckResponse] +} + +// Check calls health.v1.HealthService.Check. +func (c *healthServiceClient) Check(ctx context.Context, req *connect.Request[v1.HealthCheckRequest]) (*connect.Response[v1.HealthCheckResponse], error) { + return c.check.CallUnary(ctx, req) +} + +// HealthServiceHandler is an implementation of the health.v1.HealthService service. +type HealthServiceHandler interface { + Check(context.Context, *connect.Request[v1.HealthCheckRequest]) (*connect.Response[v1.HealthCheckResponse], error) +} + +// NewHealthServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewHealthServiceHandler(svc HealthServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + healthServiceMethods := v1.File_health_v1_health_proto.Services().ByName("HealthService").Methods() + healthServiceCheckHandler := connect.NewUnaryHandler( + HealthServiceCheckProcedure, + svc.Check, + connect.WithSchema(healthServiceMethods.ByName("Check")), + connect.WithHandlerOptions(opts...), + ) + return "/health.v1.HealthService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case HealthServiceCheckProcedure: + healthServiceCheckHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedHealthServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedHealthServiceHandler struct{} + +func (UnimplementedHealthServiceHandler) Check(context.Context, *connect.Request[v1.HealthCheckRequest]) (*connect.Response[v1.HealthCheckResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("health.v1.HealthService.Check is not implemented")) +} diff --git a/proto/health/v1/health.proto b/proto/health/v1/health.proto new file mode 100644 index 00000000..ab9f6ca8 --- /dev/null +++ b/proto/health/v1/health.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package health.v1; + +option go_package = "github.com/go-authgate/authgate/internal/gen/health/v1;healthv1"; + +message HealthCheckRequest {} + +message HealthCheckResponse { + ServingStatus status = 1; + string database = 2; + + enum ServingStatus { + SERVING_STATUS_UNSPECIFIED = 0; + SERVING_STATUS_SERVING = 1; + SERVING_STATUS_NOT_SERVING = 2; + } +} + +service HealthService { + rpc Check(HealthCheckRequest) returns (HealthCheckResponse) {} +}