Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion run/service-health/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
)
16 changes: 13 additions & 3 deletions run/service-health/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,18 @@ <h2>Readiness probe is {{.HealthStr}} on this instance!</h2>
period seconds: <code>{{.ReadinessProbeConfig.PeriodSeconds}}</code>,
success threshold: <code>{{.ReadinessProbeConfig.SuccessThreshold}}</code>,
failure threshold: <code>{{.ReadinessProbeConfig.FailureThreshold}}</code>,
http path: <code>{{.ReadinessProbeConfig.HttpGetAction.Path}}</code>,
http port: <code>{{.ReadinessProbeConfig.HttpGetAction.Port}}</code>.
{{if .ReadinessProbeConfig.HttpGetAction.Path}}
http path: <code>{{.ReadinessProbeConfig.HttpGetAction.Path}}</code>,
{{end}}
{{if .ReadinessProbeConfig.HttpGetAction.Port}}
http port: <code>{{.ReadinessProbeConfig.HttpGetAction.Port}}</code>.
{{end}}
{{if .ReadinessProbeConfig.GrpcAction.Service}}
grpc path: <code>{{.ReadinessProbeConfig.GrpcAction.Service}}</code>,
{{end}}
{{if .ReadinessProbeConfig.GrpcAction.Port}}
grpc port: <code>{{.ReadinessProbeConfig.GrpcAction.Port}}</code>.
{{end}}
</p>
{{end}}

Expand Down Expand Up @@ -217,4 +227,4 @@ <h3>Serving instances for this service:</h3>
</script>
</body>

</html>
</html>
45 changes: 43 additions & 2 deletions run/service-health/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"html/template"
"io"
"log"
"net"
"net/http"
"os"
"slices"
Expand All @@ -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 (
Expand Down Expand Up @@ -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 {
Expand All @@ -97,6 +105,10 @@ type Service struct {
} `json:"spec"`
}

type healthServer struct {
healthpb.UnimplementedHealthServer
}

func init() {
var err error

Expand Down Expand Up @@ -205,6 +217,8 @@ func main() {
}
}()

go startGrpcServer(8081)

port := os.Getenv("PORT")
if port == "" {
port = "8080"
Expand All @@ -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
Expand Down
62 changes: 61 additions & 1 deletion run/testing/service_health.e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
Comment on lines +90 to +148
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is significant code duplication between TestServiceHealthHttp and this new TestServiceHealthGrpc test. Consider refactoring them into a single table-driven test to improve maintainability and reduce boilerplate.

You could define a slice of test cases, with each case specifying the readiness probe configuration, and then loop through them in a single test function using t.Run for each subtest.

Example structure:

func TestServiceHealth(t *testing.T) {
	tc := testutil.EndToEndTest(t)

	tests := []struct {
		name    string
		probe   *cloudrunci.ReadinessProbe
	}{
		{
			name: "HTTP",
			probe: &cloudrunci.ReadinessProbe{
				// ... HTTP probe config
			},
		},
		{
			name: "gRPC",
			probe: &cloudrunci.ReadinessProbe{
				// ... gRPC probe config
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			// ... shared test logic using test.probe
		})
	}
}