diff --git a/internal/cloudrunci/cloudrunci.go b/internal/cloudrunci/cloudrunci.go index 229c436b94..70abfcb230 100644 --- a/internal/cloudrunci/cloudrunci.go +++ b/internal/cloudrunci/cloudrunci.go @@ -49,6 +49,28 @@ const ( defaultRegistryName = "cloudrunci" ) +// HTTPGetProbe describes a probe definition using HTTP Get. +type HTTPGetProbe struct { + Path string + Port int +} + +// GRPCProbe describes a probe definition using gRPC. +type GRPCProbe struct { + Port int + Service string +} + +// ReadinessProbe describes the readiness probe for a Cloud Run service. +type ReadinessProbe struct { + TimeoutSeconds int + PeriodSeconds int + SuccessThreshold int + FailureThreshold int + HttpGet *HTTPGetProbe + GRPC *GRPCProbe +} + // Service describes a Cloud Run service type Service struct { // Name is an ID, used for logging and to generate a unique version to this run. @@ -85,6 +107,9 @@ type Service struct { // Location to deploy the Service, and related artifacts Location string + + // ReadinessProbe description + Readiness *ReadinessProbe } // runID is an identifier that changes between runs. @@ -272,7 +297,7 @@ func (s *Service) validate() error { // revision returns the revision that the service will be deployed to. // NOTE: Until traffic splitting is available, this will be used as the service name. -func (s *Service) version() string { +func (s *Service) Version() string { return s.Name + "-" + runID } @@ -293,7 +318,7 @@ func (s *Service) Deploy() error { } if _, err := gcloud(s.operationLabel(labelOperationDeploy), s.deployCmd()); err != nil { - return fmt.Errorf("gcloud: %s: %q", s.version(), err) + return fmt.Errorf("gcloud: %s: %q", s.Version(), err) } s.deployed = true @@ -339,7 +364,7 @@ func (s *Service) Clean() error { } if _, err := gcloud(s.operationLabel(labelOperationDeleteService), s.deleteServiceCmd()); err != nil { - return fmt.Errorf("gcloud: %v: %q", s.version(), err) + return fmt.Errorf("gcloud: %v: %q", s.Version(), err) } s.deployed = false @@ -347,7 +372,7 @@ func (s *Service) Clean() error { if s.built { _, err := gcloud(s.operationLabel("delete container image"), s.deleteImageCmd()) if err != nil { - return fmt.Errorf("gcloud: %v: %q", s.version(), err) + return fmt.Errorf("gcloud: %v: %q", s.Version(), err) } s.built = false } @@ -365,7 +390,7 @@ func (s *Service) deployCmd() *exec.Cmd { "alpha", // TODO until --use-http2 goes GA "run", "deploy", - s.version(), + s.Version(), "--project", s.ProjectID, "--image", @@ -384,6 +409,40 @@ func (s *Service) deployCmd() *exec.Cmd { args = append(args, "--use-http2") } + if s.Readiness != nil { + var readinessProbeParts []string + if s.Readiness.TimeoutSeconds > 0 { + readinessProbeParts = append(readinessProbeParts, fmt.Sprintf("timeoutSeconds=%d", s.Readiness.TimeoutSeconds)) + } + if s.Readiness.PeriodSeconds > 0 { + readinessProbeParts = append(readinessProbeParts, fmt.Sprintf("periodSeconds=%d", s.Readiness.PeriodSeconds)) + } + if s.Readiness.SuccessThreshold > 0 { + readinessProbeParts = append(readinessProbeParts, fmt.Sprintf("successThreshold=%d", s.Readiness.SuccessThreshold)) + } + if s.Readiness.FailureThreshold > 0 { + readinessProbeParts = append(readinessProbeParts, fmt.Sprintf("failureThreshold=%d", s.Readiness.FailureThreshold)) + } + if s.Readiness.HttpGet != nil { + if s.Readiness.HttpGet.Path != "" { + readinessProbeParts = append(readinessProbeParts, fmt.Sprintf("httpGet.path=%s", s.Readiness.HttpGet.Path)) + } + if s.Readiness.HttpGet.Port > 0 { + readinessProbeParts = append(readinessProbeParts, fmt.Sprintf("httpGet.port=%d", s.Readiness.HttpGet.Port)) + } + } else if s.Readiness.GRPC != nil { + if s.Readiness.GRPC.Service != "" { + readinessProbeParts = append(readinessProbeParts, fmt.Sprintf("grpc.service=%s", s.Readiness.GRPC.Service)) + } + if s.Readiness.GRPC.Port > 0 { + readinessProbeParts = append(readinessProbeParts, fmt.Sprintf("grpc.port=%d", s.Readiness.GRPC.Port)) + } + } + if len(readinessProbeParts) > 0 { + args = append(args, "--readiness-probe="+strings.Join(readinessProbeParts, ",")) + } + } + // NOTE: if the "beta" component is not available, and this is run in parallel, // gcloud will attempt to install those components multiple // times and will eventually fail on IO. @@ -439,7 +498,7 @@ func (s *Service) deleteServiceCmd() *exec.Cmd { "run", "services", "delete", - s.version(), + s.Version(), "--project", s.ProjectID, }, s.Platform.CommandFlags()...) @@ -458,7 +517,7 @@ func (s *Service) urlCmd() *exec.Cmd { "run", "services", "describe", - s.version(), + s.Version(), "--project", s.ProjectID, "--format", @@ -481,7 +540,7 @@ func (s *Service) LogEntries(filter string, find string, maxAttempts int) (bool, } defer client.Close() - preparedFilter := fmt.Sprintf(`resource.type="cloud_run_revision" resource.labels.service_name="%s" %s`, s.version(), filter) + preparedFilter := fmt.Sprintf(`resource.type="cloud_run_revision" resource.labels.service_name="%s" %s`, s.Version(), filter) log.Printf("Using log filter: %s\n", preparedFilter) log.Println("Waiting for logs...") diff --git a/internal/cloudrunci/cloudrunci_test.go b/internal/cloudrunci/cloudrunci_test.go index 0edb321aa2..6f4b7cc984 100644 --- a/internal/cloudrunci/cloudrunci_test.go +++ b/internal/cloudrunci/cloudrunci_test.go @@ -111,6 +111,46 @@ func TestDeployArgs(t *testing.T) { } } +func TestDeployArgsReadinessProbe(t *testing.T) { + tests := []struct { + name string + readiness *ReadinessProbe + wantArgs []string // Expected arguments in the gcloud command + }{ + { + name: "HTTPGet probe", + readiness: &ReadinessProbe{ + TimeoutSeconds: 10, PeriodSeconds: 5, SuccessThreshold: 1, FailureThreshold: 3, + HttpGet: &HTTPGetProbe{Path: "/healthz", Port: 8080}, + }, + wantArgs: []string{"--readiness-probe=timeoutSeconds=10,periodSeconds=5,successThreshold=1,failureThreshold=3,httpGet.path=/healthz,httpGet.port=8080"}, + }, + { + name: "GRPC probe", + readiness: &ReadinessProbe{ + TimeoutSeconds: 10, PeriodSeconds: 5, SuccessThreshold: 1, FailureThreshold: 3, + GRPC: &GRPCProbe{Port: 50051, Service: "myservice"}, + }, + wantArgs: []string{"--readiness-probe=timeoutSeconds=10,periodSeconds=5,successThreshold=1,failureThreshold=3,grpc.service=myservice,grpc.port=50051"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + service := NewService("my-service", "my-project") + service.Image = "gcr.io/my-project/my-service" + service.Readiness = test.readiness + + cmd := service.deployCmd() + for _, wantArg := range test.wantArgs { + if !contains(cmd.Args, wantArg) { + t.Errorf("deployCmd() args missing expected readiness probe arg: %s, got: %v", wantArg, cmd.Args) + } + } + }) + } +} + // contains searches for a string value in a string slice. func contains(haystack []string, needle string) bool { for _, i := range haystack {