diff --git a/run/service-health/go.mod b/run/service-health/go.mod index dc45bad154..5b0775ae35 100644 --- a/run/service-health/go.mod +++ b/run/service-health/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( cloud.google.com/go/storage v1.55.0 google.golang.org/api v0.235.0 + google.golang.org/grpc v1.72.1 ) require ( @@ -52,6 +53,5 @@ require ( google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect - google.golang.org/grpc v1.72.1 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/run/service-health/layout.html b/run/service-health/layout.html index b0b02e72c3..bfc4c860a5 100644 --- a/run/service-health/layout.html +++ b/run/service-health/layout.html @@ -69,8 +69,18 @@

Readiness probe is {{.HealthStr}} on this instance!

period seconds: {{.ReadinessProbeConfig.PeriodSeconds}}, success threshold: {{.ReadinessProbeConfig.SuccessThreshold}}, failure threshold: {{.ReadinessProbeConfig.FailureThreshold}}, - http path: {{.ReadinessProbeConfig.HttpGetAction.Path}}, - http port: {{.ReadinessProbeConfig.HttpGetAction.Port}}. + {{if .ReadinessProbeConfig.HttpGetAction.Path}} + http path: {{.ReadinessProbeConfig.HttpGetAction.Path}}, + {{end}} + {{if .ReadinessProbeConfig.HttpGetAction.Port}} + http port: {{.ReadinessProbeConfig.HttpGetAction.Port}}. + {{end}} + {{if .ReadinessProbeConfig.GrpcAction.Service}} + grpc path: {{.ReadinessProbeConfig.GrpcAction.Service}}, + {{end}} + {{if .ReadinessProbeConfig.GrpcAction.Port}} + grpc port: {{.ReadinessProbeConfig.GrpcAction.Port}}. + {{end}}

{{end}} @@ -217,4 +227,4 @@

Serving instances for this service:

- \ No newline at end of file + diff --git a/run/service-health/main.go b/run/service-health/main.go index 3dae8cdcdd..80d4c7cd2e 100644 --- a/run/service-health/main.go +++ b/run/service-health/main.go @@ -21,6 +21,7 @@ import ( "html/template" "io" "log" + "net" "net/http" "os" "slices" @@ -29,6 +30,9 @@ import ( "cloud.google.com/go/storage" "google.golang.org/api/iterator" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + healthpb "google.golang.org/grpc/health/grpc_health_v1" ) var ( @@ -78,9 +82,13 @@ type ReadinessProbeConfig struct { SuccessThreshold int `json:"successThreshold"` FailureThreshold int `json:"failureThreshold"` HttpGetAction struct { - Path string `json:"path"` - Port int `json:"port"` + Path *string `json:"path"` + Port *int `json:"port"` } `json:"httpGet"` + GrpcAction struct { + Service *string `json:"service"` + Port *int `json:"port"` + } `json:"grpc"` } type Service struct { @@ -97,6 +105,10 @@ type Service struct { } `json:"spec"` } +type healthServer struct { + healthpb.UnimplementedHealthServer +} + func init() { var err error @@ -205,6 +217,8 @@ func main() { } }() + go startGrpcServer(8081) + port := os.Getenv("PORT") if port == "" { port = "8080" @@ -222,6 +236,33 @@ func main() { } } +func startGrpcServer(port int) { + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + log.Fatalf("grpc failed to listen: %v", err) + } + + s := grpc.NewServer() + healthpb.RegisterHealthServer(s, &healthServer{}) + + log.Printf("grpc listening on port %d", port) + if err := s.Serve(lis); err != nil { + log.Fatalf("grpc failed to serve: %v", err) + } +} + +func (s *healthServer) Check(ctx context.Context, in *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) { + if !readinessEnabled { + return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_UNKNOWN}, grpc.Errorf(codes.FailedPrecondition, "readiness not enabled") + } + + if isHealthy { + return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil + } else { + return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_NOT_SERVING}, nil + } +} + func cache() error { var sortedInstances []InstanceView var sortedString []string diff --git a/run/testing/service_health.e2e_test.go b/run/testing/service_health.e2e_test.go index fb40424972..f409a9448f 100644 --- a/run/testing/service_health.e2e_test.go +++ b/run/testing/service_health.e2e_test.go @@ -26,7 +26,7 @@ import ( "github.com/GoogleCloudPlatform/golang-samples/internal/testutil" ) -func TestServiceHealth(t *testing.T) { +func TestServiceHealthHttp(t *testing.T) { tc := testutil.EndToEndTest(t) service := cloudrunci.NewService("service-health", tc.ProjectID) @@ -86,3 +86,63 @@ func TestServiceHealth(t *testing.T) { t.Fatalf("testutil.DeleteBucketIfExists: %v", err) } } + +func TestServiceHealthGrpc(t *testing.T) { + tc := testutil.EndToEndTest(t) + + service := cloudrunci.NewService("service-health", tc.ProjectID) + service.Readiness = &cloudrunci.ReadinessProbe{ + TimeoutSeconds: 1, + PeriodSeconds: 1, + SuccessThreshold: 1, + FailureThreshold: 1, + GRPC: &cloudrunci.GRPCProbe{ + Port: 8081, + }, + } + service.Dir = "../service-health" + service.AsBuildpack = true + service.Platform.CommandFlags() + + if err := service.Deploy(); err != nil { + t.Fatalf("service.Deploy %q: %v", service.Name, err) + } + defer func(service *cloudrunci.Service) { + err := service.Clean() + if err != nil { + t.Fatalf("service.Clean %q: %v", service.Name, err) + } + }(service) + + resp, err := service.Request("GET", "/are_you_ready") + if err != nil { + t.Fatalf("request: %v", err) + } + + out, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("io.ReadAll: %v", err) + } + + if got, want := string(out), "HEALTHY"; got != want { + t.Errorf("body: got %q, want %q", got, want) + } + + if got := resp.StatusCode; got != http.StatusOK { + t.Errorf("response status: got %d, want %d", got, http.StatusOK) + } + + ctx := context.Background() + c, err := storage.NewClient(ctx) + if err != nil { + t.Fatalf("storage.NewClient: %v", err) + } + defer c.Close() + bucketName := os.Getenv("GOLANG_SAMPLES_PROJECT_ID") + "-" + service.Version() + t.Logf("Deleting bucket: %s", bucketName) + + err = testutil.DeleteBucketIfExists(ctx, c, bucketName) + if err != nil { + t.Fatalf("testutil.DeleteBucketIfExists: %v", err) + } +}