Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enhance Cloud Run deploy command with advanced configuration options #40

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: Add robust service name validation for Cloud Run deployments
- Implement comprehensive service name validation for Cloud Run
- Add validation checks for service name length, character composition, and format
- Ensure service names meet Cloud Run API requirements
- Add logging for service name validation process
- Enhance error handling and reporting for invalid service names
  • Loading branch information
dijarvrella committed Feb 27, 2025
commit 2edbb14e63c5f4a41577a439a8f93e4b572fbd1f
47 changes: 47 additions & 0 deletions cmd/cloud-run/deploy.go
Original file line number Diff line number Diff line change
@@ -20,6 +20,10 @@ import (
"github.com/dijarvrella/cicd-images/cmd/cloud-run/pkg/config"
"github.com/dijarvrella/cicd-images/cmd/cloud-run/pkg/deploy"

"fmt"
"log"
"unicode"

"github.com/spf13/cobra"
"google.golang.org/api/option"
"google.golang.org/api/run/v2"
@@ -149,6 +153,13 @@ Examples:
func deployService(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()

// Add more robust validation for service name to ensure it meets Cloud Run requirements
if !isValidServiceName(opts.Service) {
return fmt.Errorf("invalid service name: %s. Service names must contain only lowercase letters, numbers, and hyphens, must begin with a letter, cannot end with a hyphen, and must be less than 50 characters", opts.Service)
}

log.Printf("Service name validation passed for: %s", opts.Service)

cloudbuildClient, err := cloudbuild.NewClient(ctx)
if err != nil {
return err
@@ -176,9 +187,45 @@ func deployService(cmd *cobra.Command, _ []string) error {

servicesClient := run.NewProjectsLocationsServicesService(runService)

log.Printf("Using service name: %s", opts.Service)
err = deploy.CreateOrUpdateServiceV2(servicesClient, projectID, region, opts)
if err != nil {
return err
}
return deploy.WaitForServiceReadyV2(ctx, servicesClient, projectID, region, opts.Service)
}

// isValidServiceName validates that a service name meets Cloud Run requirements:
// - Only lowercase letters, numbers, and hyphens
// - Must begin with a letter
// - Cannot end with a hyphen
// - Must be less than 50 characters
func isValidServiceName(name string) bool {
if len(name) == 0 || len(name) >= 50 {
log.Printf("Service name validation failed: length check. Name: %s, Length: %d", name, len(name))
return false
}

// Must start with a letter
if !unicode.IsLetter(rune(name[0])) {
log.Printf("Service name validation failed: must start with a letter. Name: %s", name)
return false
}

// Cannot end with a hyphen
if name[len(name)-1] == '-' {
log.Printf("Service name validation failed: cannot end with hyphen. Name: %s", name)
return false
}

// Only lowercase letters, numbers, and hyphens allowed
for i, r := range name {
if !unicode.IsLower(r) && !unicode.IsDigit(r) && r != '-' {
log.Printf("Service name validation failed: invalid character at position %d: '%c'. Name: %s", i, r, name)
return false
}
}

log.Printf("Service name validation successful for: %s", name)
return true
}
45 changes: 43 additions & 2 deletions cmd/cloud-run/pkg/deploy/deploy.go
Original file line number Diff line number Diff line change
@@ -108,11 +108,19 @@ func WaitForServiceReady(ctx context.Context, runAPIClient *runv1.APIService, pr
}

// parseSecretReference parses a secret reference in the format projects/PROJECT_ID/secrets/SECRET_NAME/versions/VERSION
// or in the format SECRET_NAME:VERSION
// or the simpler format SECRET_NAME:VERSION
// and returns the secret name and version
func parseSecretReference(secretRef string) (secretName, version string) {
log.Printf("Parsing secret reference: %s", secretRef)

// Remove any whitespace
secretRef = strings.TrimSpace(secretRef)

// Handle empty input
if secretRef == "" {
return "", "latest"
}

// Check if it's in the format projects/PROJECT_ID/secrets/SECRET_NAME/versions/VERSION
if strings.Contains(secretRef, "projects/") && strings.Contains(secretRef, "/secrets/") {
parts := strings.Split(secretRef, "/secrets/")
@@ -691,8 +699,18 @@ func CreateOrUpdateServiceV2(servicesClient *run.ProjectsLocationsServicesServic
log.Printf("Creating a new service %s\n", opts.Service)
service := buildServiceDefinitionV2(projectID, opts)

// IMPORTANT: Do not set the Name field directly in the service object
// The API will extract the service_id from the parent path
createCall := servicesClient.Create(parent, service)
_, err = createCall.Do()

// Set the service ID explicitly as a parameter rather than in the body
// This ensures the right format is used for the V2 API
log.Printf("Using service ID: %s for Cloud Run V2 API", opts.Service)
_, err = createCall.ServiceId(opts.Service).Do()
if err != nil {
log.Printf("Error creating service: %v", err)
return err
}

if err != nil {
return err
@@ -929,15 +947,31 @@ func processSecretsV2(service *run.GoogleCloudRunV2Service, secrets map[string]s

container := service.Template.Containers[0]

// Initialize env array if nil
if container.Env == nil {
container.Env = make([]*run.GoogleCloudRunV2EnvVar, 0)
}

// Process each secret
for key, secretRef := range secrets {
secretName, version := parseSecretReference(secretRef)

// Make sure we have valid values
if secretName == "" {
log.Printf("Warning: Invalid secret reference format for key %s: %s. Expected format: SECRET:VERSION", key, secretRef)
continue
}

// Check if it's a volume mount (path starts with /)
if strings.HasPrefix(key, "/") {
// Create volume if it doesn't exist
volumeName := fmt.Sprintf("secret-volume-%s", secretName)

// Initialize volumes array if nil
if service.Template.Volumes == nil {
service.Template.Volumes = make([]*run.GoogleCloudRunV2Volume, 0)
}

// Find or create the volume
var volume *run.GoogleCloudRunV2Volume
for _, v := range service.Template.Volumes {
@@ -963,6 +997,11 @@ func processSecretsV2(service *run.GoogleCloudRunV2Service, secrets map[string]s
service.Template.Volumes = append(service.Template.Volumes, volume)
}

// Initialize volume mounts array if nil
if container.VolumeMounts == nil {
container.VolumeMounts = make([]*run.GoogleCloudRunV2VolumeMount, 0)
}

// Create volume mount
mount := &run.GoogleCloudRunV2VolumeMount{
Name: volumeName,
@@ -1013,6 +1052,8 @@ func processSecretsV2(service *run.GoogleCloudRunV2Service, secrets map[string]s

// buildServiceDefinitionV2 creates a new Cloud Run service definition using the v2 API
func buildServiceDefinitionV2(projectID string, opts config.DeployOptions) *run.GoogleCloudRunV2Service {
// Create a new service without setting the Name field
// The Name will be properly set by the API based on the parent and serviceId parameters
service := &run.GoogleCloudRunV2Service{
Template: &run.GoogleCloudRunV2RevisionTemplate{
Containers: []*run.GoogleCloudRunV2Container{