Skip to content
Merged
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
75 changes: 67 additions & 8 deletions internal/cloudrunci/cloudrunci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -339,15 +364,15 @@ 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

// If s.built is false no image was created or is not managed by cloudrun-ci.
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
}
Expand All @@ -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",
Expand All @@ -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.
Expand Down Expand Up @@ -439,7 +498,7 @@ func (s *Service) deleteServiceCmd() *exec.Cmd {
"run",
"services",
"delete",
s.version(),
s.Version(),
"--project",
s.ProjectID,
}, s.Platform.CommandFlags()...)
Expand All @@ -458,7 +517,7 @@ func (s *Service) urlCmd() *exec.Cmd {
"run",
"services",
"describe",
s.version(),
s.Version(),
"--project",
s.ProjectID,
"--format",
Expand All @@ -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...")
Expand Down
40 changes: 40 additions & 0 deletions internal/cloudrunci/cloudrunci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading