Skip to content

Commit d008f50

Browse files
committed
feat(cloudrun): update sample service for cloud run readiness probe to have grpc support
1 parent 7f70436 commit d008f50

File tree

4 files changed

+124
-7
lines changed

4 files changed

+124
-7
lines changed

run/service-health/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.23.0
55
require (
66
cloud.google.com/go/storage v1.55.0
77
google.golang.org/api v0.235.0
8+
google.golang.org/grpc v1.72.1
89
)
910

1011
require (
@@ -52,6 +53,5 @@ require (
5253
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
5354
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect
5455
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
55-
google.golang.org/grpc v1.72.1 // indirect
5656
google.golang.org/protobuf v1.36.6 // indirect
5757
)

run/service-health/layout.html

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,18 @@ <h2>Readiness probe is {{.HealthStr}} on this instance!</h2>
6969
period seconds: <code>{{.ReadinessProbeConfig.PeriodSeconds}}</code>,
7070
success threshold: <code>{{.ReadinessProbeConfig.SuccessThreshold}}</code>,
7171
failure threshold: <code>{{.ReadinessProbeConfig.FailureThreshold}}</code>,
72-
http path: <code>{{.ReadinessProbeConfig.HttpGetAction.Path}}</code>,
73-
http port: <code>{{.ReadinessProbeConfig.HttpGetAction.Port}}</code>.
72+
{{if .ReadinessProbeConfig.HttpGetAction.Path}}
73+
http path: <code>{{.ReadinessProbeConfig.HttpGetAction.Path}}</code>,
74+
{{end}}
75+
{{if .ReadinessProbeConfig.HttpGetAction.Port}}
76+
http port: <code>{{.ReadinessProbeConfig.HttpGetAction.Port}}</code>.
77+
{{end}}
78+
{{if .ReadinessProbeConfig.GrpcAction.Service}}
79+
grpc path: <code>{{.ReadinessProbeConfig.GrpcAction.Service}}</code>,
80+
{{end}}
81+
{{if .ReadinessProbeConfig.GrpcAction.Port}}
82+
grpc port: <code>{{.ReadinessProbeConfig.GrpcAction.Port}}</code>.
83+
{{end}}
7484
</p>
7585
{{end}}
7686

@@ -217,4 +227,4 @@ <h3>Serving instances for this service:</h3>
217227
</script>
218228
</body>
219229

220-
</html>
230+
</html>

run/service-health/main.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"html/template"
2222
"io"
2323
"log"
24+
"net"
2425
"net/http"
2526
"os"
2627
"slices"
@@ -29,6 +30,9 @@ import (
2930

3031
"cloud.google.com/go/storage"
3132
"google.golang.org/api/iterator"
33+
"google.golang.org/grpc"
34+
"google.golang.org/grpc/codes"
35+
healthpb "google.golang.org/grpc/health/grpc_health_v1"
3236
)
3337

3438
var (
@@ -78,9 +82,13 @@ type ReadinessProbeConfig struct {
7882
SuccessThreshold int `json:"successThreshold"`
7983
FailureThreshold int `json:"failureThreshold"`
8084
HttpGetAction struct {
81-
Path string `json:"path"`
82-
Port int `json:"port"`
85+
Path *string `json:"path"`
86+
Port *int `json:"port"`
8387
} `json:"httpGet"`
88+
GrpcAction struct {
89+
Service *string `json:"service"`
90+
Port *int `json:"port"`
91+
} `json:"grpc"`
8492
}
8593

8694
type Service struct {
@@ -97,6 +105,8 @@ type Service struct {
97105
} `json:"spec"`
98106
}
99107

108+
type healthServer struct{}
109+
100110
func init() {
101111
var err error
102112

@@ -205,6 +215,8 @@ func main() {
205215
}
206216
}()
207217

218+
go startGrpcServer(8081)
219+
208220
port := os.Getenv("PORT")
209221
if port == "" {
210222
port = "8080"
@@ -222,6 +234,41 @@ func main() {
222234
}
223235
}
224236

237+
func startGrpcServer(port int) {
238+
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
239+
if err != nil {
240+
log.Fatalf("grpc failed to listen: %v", err)
241+
}
242+
243+
s := grpc.NewServer()
244+
healthpb.RegisterHealthServer(s, &healthServer{})
245+
246+
log.Printf("grpc listening on port %d", port)
247+
if err := s.Serve(lis); err != nil {
248+
log.Fatalf("grpc failed to serve: %v", err)
249+
}
250+
}
251+
252+
func (s *healthServer) Check(ctx context.Context, in *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
253+
if !readinessEnabled {
254+
return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_UNKNOWN}, grpc.Errorf(codes.FailedPrecondition, "readiness not enabled")
255+
}
256+
257+
if isHealthy {
258+
return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil
259+
} else {
260+
return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_NOT_SERVING}, nil
261+
}
262+
}
263+
264+
func (s *healthServer) List(ctx context.Context, in *healthpb.HealthListRequest) (*healthpb.HealthListResponse, error) {
265+
return &healthpb.HealthListResponse{}, grpc.Errorf(codes.Unimplemented, "List is not implemented")
266+
}
267+
268+
func (s *healthServer) Watch(in *healthpb.HealthCheckRequest, srv healthpb.Health_WatchServer) error {
269+
return grpc.Errorf(codes.Unimplemented, "Watch is not implemented")
270+
}
271+
225272
func cache() error {
226273
var sortedInstances []InstanceView
227274
var sortedString []string

run/testing/service_health.e2e_test.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
"github.com/GoogleCloudPlatform/golang-samples/internal/testutil"
2727
)
2828

29-
func TestServiceHealth(t *testing.T) {
29+
func TestServiceHealthHttp(t *testing.T) {
3030
tc := testutil.EndToEndTest(t)
3131

3232
service := cloudrunci.NewService("service-health", tc.ProjectID)
@@ -86,3 +86,63 @@ func TestServiceHealth(t *testing.T) {
8686
t.Fatalf("testutil.DeleteBucketIfExists: %v", err)
8787
}
8888
}
89+
90+
func TestServiceHealthGrpc(t *testing.T) {
91+
tc := testutil.EndToEndTest(t)
92+
93+
service := cloudrunci.NewService("service-health", tc.ProjectID)
94+
service.Readiness = &cloudrunci.ReadinessProbe{
95+
TimeoutSeconds: 1,
96+
PeriodSeconds: 1,
97+
SuccessThreshold: 1,
98+
FailureThreshold: 1,
99+
GRPC: &cloudrunci.GRPCProbe{
100+
Port: 8081,
101+
},
102+
}
103+
service.Dir = "../service-health"
104+
service.AsBuildpack = true
105+
service.Platform.CommandFlags()
106+
107+
if err := service.Deploy(); err != nil {
108+
t.Fatalf("service.Deploy %q: %v", service.Name, err)
109+
}
110+
defer func(service *cloudrunci.Service) {
111+
err := service.Clean()
112+
if err != nil {
113+
t.Fatalf("service.Clean %q: %v", service.Name, err)
114+
}
115+
}(service)
116+
117+
resp, err := service.Request("GET", "/are_you_ready")
118+
if err != nil {
119+
t.Fatalf("request: %v", err)
120+
}
121+
122+
out, err := io.ReadAll(resp.Body)
123+
if err != nil {
124+
t.Fatalf("io.ReadAll: %v", err)
125+
}
126+
127+
if got, want := string(out), "HEALTHY"; got != want {
128+
t.Errorf("body: got %q, want %q", got, want)
129+
}
130+
131+
if got := resp.StatusCode; got != http.StatusOK {
132+
t.Errorf("response status: got %d, want %d", got, http.StatusOK)
133+
}
134+
135+
ctx := context.Background()
136+
c, err := storage.NewClient(ctx)
137+
if err != nil {
138+
t.Fatalf("storage.NewClient: %v", err)
139+
}
140+
defer c.Close()
141+
bucketName := os.Getenv("GOLANG_SAMPLES_PROJECT_ID") + "-" + service.Version()
142+
t.Logf("Deleting bucket: %s", bucketName)
143+
144+
err = testutil.DeleteBucketIfExists(ctx, c, bucketName)
145+
if err != nil {
146+
t.Fatalf("testutil.DeleteBucketIfExists: %v", err)
147+
}
148+
}

0 commit comments

Comments
 (0)