From 9dd1dd7d194480c0c7d5280ea1387b8ea7295d07 Mon Sep 17 00:00:00 2001 From: gcristea-temporal Date: Mon, 20 Jul 2026 10:02:50 -0700 Subject: [PATCH 1/2] COM-132: Add GCP Cloud Run support to worker deployment version command (#1103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the AWS Lambda support: four `--gcp-cloud-run-*` flags on `create-version` and `update-version-compute-config`, writing a gcp-cloud-run ComputeConfig provider. ## Related issues ## What changed? ### Add GCP Cloud Run worker pools as a compute provider Lets an operator point a Worker Deployment Version at a GCP Cloud Run worker pool, mirroring the existing AWS Lambda support 1:1. CLI-only — serializes the four detail keys WCI reads (`project`, `region`, `worker_pool`, `service_account`) ``` temporal worker deployment create-version \ --deployment-name YourDeployment --build-id YourBuildID \ --gcp-cloud-run-project YourProject \ --gcp-cloud-run-region us-central1 \ --gcp-cloud-run-worker-pool YourWorkerPool \ --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com ``` ### Changes - **commands.yaml / commands.gen.go**: four `--gcp-cloud-run-*` flags on `create-version` and `update-version-compute-config`, plus usage examples. - **commands.worker.deployment.go**: GCP validate/payload helpers and a shared `computeProviderConfig` selector that enforces AWS/GCP mutual exclusivity and dispatches on the trigger flag. Both `run` methods rewired. - **tests**: GCP validation + mutual-exclusivity cases folded into `TestCreateWorkerDeploymentVersion_Errors`; a skipped round-trip test mirrors the Lambda one (needs real GCP fixtures). ## Checklist **Stability** - [ ] Breaking changes are marked with 💥 in the PR title and release notes - [ ] Changes to JSON output (`-o json` / `-o jsonl`) are treated as breaking changes **Design** - [x] This feature does not depend on Cloud-only APIs or behavior (it works against an OSS server) - [ ] New commands follow `temporal ` structure (e.g. `temporal workflow start`) - [x] New flags are named after the API concept, not the implementation mechanism (good: `--search-attribute`, bad: `--index-field`) - [x] New flags don't duplicate an existing flag that serves the same purpose - [x] New flags do not have short aliases without strong justification - [ ] Experimental features are marked with `(Experimental)` in `commands.yaml` **Help text** (see style guide at the top of `commands.yaml`) - [ ] All flags shown in help text and examples are implemented and functional - [x] Summaries use sentence case and have no trailing period - [x] Long descriptions end with a period and include at least one example invocation - [ ] Examples use long flags (`--namespace`, not `-n`), one flag per line - [ ] Placeholder values use `YourXxx` form (`YourWorkflowId`, `YourNamespace`) **Behavior** - [x] Results go to stdout; errors and warnings go to stderr - [x] Error messages are lowercase with no trailing punctuation **Tests** - [ ] Added functional test(s) (`SharedServerSuite`) - [x] Added unit test(s) (`func TestXxx`) where applicable ## Manual tests **Setup** ``` ./temporal --profile "ns-cloud-run-test" worker deployment create-version \ --deployment-name "cloud-run-deployment" \ --build-id "1.0" \ --gcp-cloud-run-project "compute-team-sandbox" \ --gcp-cloud-run-region "us-west1" \ --gcp-cloud-run-worker-pool "omes-worker-pool" \ --gcp-cloud-run-service-account "wci-invocation-sa@compute-team-sandbox.iam.gserviceaccount.com" Successfully created worker deployment version ``` **Happy path** ``` ./temporal --profile "ns-cloud-run-test" worker deployment describe-version \ --deployment-name "cloud-run-deployment" \ --build-id "1.0" Worker Deployment Version: DeploymentName cloud-run-deployment BuildID 1.0 CreateTime 2 minutes ago RoutingChangedTime a long while ago CurrentSinceTime a long while ago RampingSinceTime a long while ago RampPercentage 0 ComputeConfigSummary gcp-cloud-run ``` **Error case** ``` ./temporal --profile "ns-cloud-run-test" worker deployment create-version \ --deployment-name "cloud-run-deployment" \ --build-id "1.0" \ --gcp-cloud-run-project "compute-team-sandbox" \ --gcp-cloud-run-region "us-west1" \ --gcp-cloud-run-worker-pool "fake-omes-worker-pool" \ --gcp-cloud-run-service-account "wci-invocation-sa@compute-team-sandbox.iam.gserviceaccount.com" Error: error creating worker deployment version: Worker Deployment Version "cloud-run-deployment.1.0" already exists ./temporal --profile "ns-cloud-run-test" worker deployment create-version \ --deployment-name "cloud-run-deployment" \ --build-id "2.0" \ --gcp-cloud-run-project "compute-team-sandbox" \ --gcp-cloud-run-region "us-west1" \ --gcp-cloud-run-worker-pool "fake-omes-worker-pool" \ --gcp-cloud-run-service-account "wci-invocation-sa@compute-team-sandbox.iam.gserviceaccount.com" Error: error creating worker deployment version: default: worker pool "projects/compute-team-sandbox/locations/us-west1/workerPools/fake-omes-worker-pool" not found: rpc error: code = NotFound desc = Resource 'fake-omes-worker-pool' of kind 'WORKER_POOL' in region 'us-west1' in project 'compute-team-sandbox' does not exist. ``` **Composition** ``` $ temporal ... $ temporal --flag ``` (cherry picked from commit 076d1d5a8b8b94a4a494b0c4ef4516460a60dfca) --- internal/temporalcli/commands.gen.go | 24 ++- .../temporalcli/commands.worker.deployment.go | 178 +++++++++++++---- ...ommands.worker.deployment.internal_test.go | 42 ++++ .../commands.worker.deployment_test.go | 187 ++++++++++++++++++ internal/temporalcli/commands.yaml | 70 ++++++- 5 files changed, 463 insertions(+), 38 deletions(-) create mode 100644 internal/temporalcli/commands.worker.deployment.internal_test.go diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index b24c6be3d..3ecadb577 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -3307,6 +3307,10 @@ type TemporalWorkerDeploymentCreateVersionCommand struct { AwsLambdaFunctionArn string AwsLambdaAssumeRoleArn string AwsLambdaAssumeRoleExternalId string + GcpCloudRunProject string + GcpCloudRunRegion string + GcpCloudRunWorkerPool string + GcpCloudRunServiceAccount string } func NewTemporalWorkerDeploymentCreateVersionCommand(cctx *CommandContext, parent *TemporalWorkerDeploymentCommand) *TemporalWorkerDeploymentCreateVersionCommand { @@ -3316,14 +3320,18 @@ func NewTemporalWorkerDeploymentCreateVersionCommand(cctx *CommandContext, paren s.Command.Use = "create-version [flags]" s.Command.Short = "Create a new Worker Deployment Version" if hasHighlighting { - s.Command.Long = "\nCreate a new Worker Deployment Version:\n\n\x1b[1mtemporal worker deployment create-version [options]\x1b[0m\n\nConfigure a Worker Deployment Version's compute configuration as needed.\nFor example, pass compute provider information for an AWS Lambda function\nthat spawns a Worker in the Worker Deployment:\n\n\x1b[1mtemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --aws-lambda-function-arn LambdaFunctionARN \\\n --aws-lambda-assume-role-arn LambdaAssumeRoleARN \\\n --aws-lambda-assume-role-external-id LambdaAssumeRoleExternalID\x1b[0m\n\nIf a Worker Deployment Version with the supplied BuildID already exists,\nthis command will return an error.\n\nReturns an error if all compute configuration fields are empty.\n\nNote: This is an experimental feature and may change in the future." + s.Command.Long = "\nCreate a new Worker Deployment Version:\n\n\x1b[1mtemporal worker deployment create-version [options]\x1b[0m\n\nConfigure a Worker Deployment Version's compute configuration as needed.\nFor example, pass compute provider information for an AWS Lambda function\nthat spawns a Worker in the Worker Deployment:\n\n\x1b[1mtemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --aws-lambda-function-arn LambdaFunctionARN \\\n --aws-lambda-assume-role-arn LambdaAssumeRoleARN \\\n --aws-lambda-assume-role-external-id LambdaAssumeRoleExternalID\x1b[0m\n\nOr pass compute provider information for a GCP Cloud Run worker pool\nthat spawns a Worker in the Worker Deployment:\n\n\x1b[1mtemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool YourWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com\x1b[0m\n\nIf a Worker Deployment Version with the supplied BuildID already exists,\nthis command will return an error.\n\nReturns an error if all compute configuration fields are empty.\n\nNote: This is an experimental feature and may change in the future." } else { - s.Command.Long = "\nCreate a new Worker Deployment Version:\n\n```\ntemporal worker deployment create-version [options]\n```\n\nConfigure a Worker Deployment Version's compute configuration as needed.\nFor example, pass compute provider information for an AWS Lambda function\nthat spawns a Worker in the Worker Deployment:\n\n```\ntemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --aws-lambda-function-arn LambdaFunctionARN \\\n --aws-lambda-assume-role-arn LambdaAssumeRoleARN \\\n --aws-lambda-assume-role-external-id LambdaAssumeRoleExternalID\n```\n\nIf a Worker Deployment Version with the supplied BuildID already exists,\nthis command will return an error.\n\nReturns an error if all compute configuration fields are empty.\n\nNote: This is an experimental feature and may change in the future." + s.Command.Long = "\nCreate a new Worker Deployment Version:\n\n```\ntemporal worker deployment create-version [options]\n```\n\nConfigure a Worker Deployment Version's compute configuration as needed.\nFor example, pass compute provider information for an AWS Lambda function\nthat spawns a Worker in the Worker Deployment:\n\n```\ntemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --aws-lambda-function-arn LambdaFunctionARN \\\n --aws-lambda-assume-role-arn LambdaAssumeRoleARN \\\n --aws-lambda-assume-role-external-id LambdaAssumeRoleExternalID\n```\n\nOr pass compute provider information for a GCP Cloud Run worker pool\nthat spawns a Worker in the Worker Deployment:\n\n```\ntemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool YourWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com\n```\n\nIf a Worker Deployment Version with the supplied BuildID already exists,\nthis command will return an error.\n\nReturns an error if all compute configuration fields are empty.\n\nNote: This is an experimental feature and may change in the future." } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.AwsLambdaFunctionArn, "aws-lambda-function-arn", "", "Qualified (contains version suffix) or unqualified AWS Lambda function ARN to invoke when there are no active pollers for task queue targets in the Worker Deployment.") s.Command.Flags().StringVar(&s.AwsLambdaAssumeRoleArn, "aws-lambda-assume-role-arn", "", "AWS IAM role ARN that the Temporal server will assume when invoking the Lambda function that spawns a new Worker in this Worker Deployment Version. Required when --aws-lambda-function-arn is specified.") s.Command.Flags().StringVar(&s.AwsLambdaAssumeRoleExternalId, "aws-lambda-assume-role-external-id", "", "Temporal server will enforce that the AWS IAM trust policy associated with the AWS IAM role specified in --aws-lambda-assume-role-arn has an aws:ExternalId condition that matches the supplied value. Required when --aws-lambda-function-arn is specified.") + s.Command.Flags().StringVar(&s.GcpCloudRunProject, "gcp-cloud-run-project", "", "GCP project ID hosting the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified.") + s.Command.Flags().StringVar(&s.GcpCloudRunRegion, "gcp-cloud-run-region", "", "Region of the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified.") + s.Command.Flags().StringVar(&s.GcpCloudRunWorkerPool, "gcp-cloud-run-worker-pool", "", "GCP Cloud Run worker pool name to scale when there are no active pollers for task queue targets in the Worker Deployment.") + s.Command.Flags().StringVar(&s.GcpCloudRunServiceAccount, "gcp-cloud-run-service-account", "", "Customer GCP service account the Temporal server impersonates to manage the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified.") s.DeploymentVersionOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { @@ -3630,6 +3638,10 @@ type TemporalWorkerDeploymentUpdateVersionComputeConfigCommand struct { AwsLambdaFunctionArn string AwsLambdaAssumeRoleArn string AwsLambdaAssumeRoleExternalId string + GcpCloudRunProject string + GcpCloudRunRegion string + GcpCloudRunWorkerPool string + GcpCloudRunServiceAccount string Remove bool } @@ -3640,14 +3652,18 @@ func NewTemporalWorkerDeploymentUpdateVersionComputeConfigCommand(cctx *CommandC s.Command.Use = "update-version-compute-config [flags]" s.Command.Short = "Update compute configuration for a Version" if hasHighlighting { - s.Command.Long = "Update compute configuration associated with a Worker Deployment\nVersion.\n\nFor example, to update the AWS Lambda function ARN associated with an\nexisting Worker Deployment Version:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-function-arn UpdatedLambdaFunctionARN\x1b[0m\n\nTo update the AWS IAM role ARN that is assumed by the serverless worker\nmanager associated with an existing Worker Deployment Version:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-assume-role-arn UpdatedRoleARN\x1b[0m\n\nIf --remove is specified, the compute configuration for the Worker\nDeployment Version will be removed:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --remove\x1b[0m\n\nIf a Worker Deployment Version with the supplied BuildID does not exist,\nthis command will return an error.\n\nNote: This is an experimental feature and may change in the future." + s.Command.Long = "Update compute configuration associated with a Worker Deployment\nVersion.\n\nFor example, to update the AWS Lambda function ARN associated with an\nexisting Worker Deployment Version:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-function-arn UpdatedLambdaFunctionARN\x1b[0m\n\nTo update the AWS IAM role ARN that is assumed by the serverless worker\nmanager associated with an existing Worker Deployment Version:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-assume-role-arn UpdatedRoleARN\x1b[0m\n\nTo update the GCP Cloud Run worker pool associated with an existing\nWorker Deployment Version:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool UpdatedWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com\x1b[0m\n\nIf --remove is specified, the compute configuration for the Worker\nDeployment Version will be removed:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --remove\x1b[0m\n\nIf a Worker Deployment Version with the supplied BuildID does not exist,\nthis command will return an error.\n\nNote: This is an experimental feature and may change in the future." } else { - s.Command.Long = "Update compute configuration associated with a Worker Deployment\nVersion.\n\nFor example, to update the AWS Lambda function ARN associated with an\nexisting Worker Deployment Version:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-function-arn UpdatedLambdaFunctionARN\n```\n\nTo update the AWS IAM role ARN that is assumed by the serverless worker\nmanager associated with an existing Worker Deployment Version:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-assume-role-arn UpdatedRoleARN\n```\n\nIf --remove is specified, the compute configuration for the Worker\nDeployment Version will be removed:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --remove\n```\n\nIf a Worker Deployment Version with the supplied BuildID does not exist,\nthis command will return an error.\n\nNote: This is an experimental feature and may change in the future." + s.Command.Long = "Update compute configuration associated with a Worker Deployment\nVersion.\n\nFor example, to update the AWS Lambda function ARN associated with an\nexisting Worker Deployment Version:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-function-arn UpdatedLambdaFunctionARN\n```\n\nTo update the AWS IAM role ARN that is assumed by the serverless worker\nmanager associated with an existing Worker Deployment Version:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-assume-role-arn UpdatedRoleARN\n```\n\nTo update the GCP Cloud Run worker pool associated with an existing\nWorker Deployment Version:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool UpdatedWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com\n```\n\nIf --remove is specified, the compute configuration for the Worker\nDeployment Version will be removed:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --remove\n```\n\nIf a Worker Deployment Version with the supplied BuildID does not exist,\nthis command will return an error.\n\nNote: This is an experimental feature and may change in the future." } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.AwsLambdaFunctionArn, "aws-lambda-function-arn", "", "Qualified (contains version suffix) or unqualified AWS Lambda function ARN to invoke when there are no active pollers for task queue targets in the Worker Deployment.") s.Command.Flags().StringVar(&s.AwsLambdaAssumeRoleArn, "aws-lambda-assume-role-arn", "", "AWS IAM role ARN that the Temporal server will assume when invoking the Lambda function that spawns a new Worker in this Worker Deployment Version. Required when --aws-lambda-function-arn is specified.") s.Command.Flags().StringVar(&s.AwsLambdaAssumeRoleExternalId, "aws-lambda-assume-role-external-id", "", "Temporal server will enforce that the AWS IAM trust policy associated with the AWS IAM role specified in --aws-lambda-assume-role-arn has an aws:ExternalId condition that matches the supplied value. Required when --aws-lambda-function-arn is specified.") + s.Command.Flags().StringVar(&s.GcpCloudRunProject, "gcp-cloud-run-project", "", "GCP project ID hosting the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified.") + s.Command.Flags().StringVar(&s.GcpCloudRunRegion, "gcp-cloud-run-region", "", "Region of the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified.") + s.Command.Flags().StringVar(&s.GcpCloudRunWorkerPool, "gcp-cloud-run-worker-pool", "", "GCP Cloud Run worker pool name to scale when there are no active pollers for task queue targets in the Worker Deployment.") + s.Command.Flags().StringVar(&s.GcpCloudRunServiceAccount, "gcp-cloud-run-service-account", "", "Customer GCP service account the Temporal server impersonates to manage the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified.") s.Command.Flags().BoolVar(&s.Remove, "remove", false, "Removes any compute configuration associated with this Worker Deployment Version.") s.DeploymentVersionOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { diff --git a/internal/temporalcli/commands.worker.deployment.go b/internal/temporalcli/commands.worker.deployment.go index 3dc25be79..a32d749ee 100644 --- a/internal/temporalcli/commands.worker.deployment.go +++ b/internal/temporalcli/commands.worker.deployment.go @@ -919,6 +919,102 @@ func awsLambdaProviderDetailsPayload( return dc.ToPayload(&providerDetails) } +func validateGCPCloudRunProviderDetails(details map[string]any) error { + for _, key := range []string{"project", "region", "worker_pool", "service_account"} { + if v, ok := details[key].(string); !ok || v == "" { + return fmt.Errorf("missing required GCP Cloud Run provider detail: %s", key) + } + } + return nil +} + +// gcpCloudRunProviderDetailsPayload returns the encoded Payload representing GCP +// Cloud Run compute provider details. All four keys are required: the first +// three name the worker-pool resource and service_account is the impersonation +// target the serverless chain depends on. +func gcpCloudRunProviderDetailsPayload( + project string, + region string, + workerPool string, + serviceAccount string, +) (*commonpb.Payload, error) { + // Map keys from temporal-auto-scaled-workers: + // https://github.com/temporalio/temporal-auto-scaled-workers/blob/d1390d11cb55b4450141ede559f7832e5620c1e4/wci/workflow/compute_provider/gcp_cloudrun.go#L21-L24 + providerDetails := map[string]any{ + "project": project, + "region": region, + "worker_pool": workerPool, + "service_account": serviceAccount, + } + err := validateGCPCloudRunProviderDetails(providerDetails) + if err != nil { + return nil, err + } + dc := converter.GetDefaultDataConverter() + return dc.ToPayload(&providerDetails) +} + +// computeProviderConfig selects the single compute provider for a Worker +// Deployment Version's "default" scaling group from the command's flags. It +// enforces that AWS Lambda and GCP Cloud Run flags are not mixed, then +// dispatches on the trigger flag (--aws-lambda-function-arn / +// --gcp-cloud-run-worker-pool). Returns an empty providerType when no provider +// flags are set, leaving the "no configuration" decision to the caller. +func computeProviderConfig( + awsLambdaFunctionARN string, + awsLambdaAssumeRoleARN string, + awsLambdaAssumeRoleExternalID string, + gcpCloudRunProject string, + gcpCloudRunRegion string, + gcpCloudRunWorkerPool string, + gcpCloudRunServiceAccount string, +) (providerType string, detailsPayload *commonpb.Payload, err error) { + awsSet := awsLambdaFunctionARN != "" || awsLambdaAssumeRoleARN != "" || awsLambdaAssumeRoleExternalID != "" + gcpSet := gcpCloudRunProject != "" || gcpCloudRunRegion != "" || gcpCloudRunWorkerPool != "" || gcpCloudRunServiceAccount != "" + if awsSet && gcpSet { + return "", nil, fmt.Errorf("cannot combine --aws-lambda-* and --gcp-cloud-run-* flags; a Worker Deployment Version supports a single compute provider") + } + + switch { + case awsLambdaFunctionARN != "": + p, err := awsLambdaProviderDetailsPayload( + awsLambdaFunctionARN, + awsLambdaAssumeRoleARN, + awsLambdaAssumeRoleExternalID, + ) + return "aws-lambda", p, err + case gcpCloudRunWorkerPool != "": + p, err := gcpCloudRunProviderDetailsPayload( + gcpCloudRunProject, + gcpCloudRunRegion, + gcpCloudRunWorkerPool, + gcpCloudRunServiceAccount, + ) + return "gcp-cloud-run", p, err + default: + return "", nil, nil + } +} + +// scalerTypeByProvider maps each compute provider to the scaling algorithm +// compatible with its launch strategy in temporal-auto-scaled-workers: aws-lambda +// is invoke-based ("no-sync"); gcp-cloud-run is worker-set-based ("rate-based"). +// WCI rejects an incompatible pairing at CreateWorkerDeploymentVersion. +var scalerTypeByProvider = map[string]string{ + "aws-lambda": "no-sync", + "gcp-cloud-run": "rate-based", +} + +// scalerTypeForProvider returns the scaling algorithm for the given provider, +// erroring if the provider has no explicit mapping so an unknown or newly-added +// provider fails loudly here rather than silently getting an incompatible scaler. +func scalerTypeForProvider(providerType string) (string, error) { + if scaler, ok := scalerTypeByProvider[providerType]; ok { + return scaler, nil + } + return "", fmt.Errorf("no scaler mapping for compute provider %q", providerType) +} + func (c *TemporalWorkerDeploymentCreateVersionCommand) run(cctx *CommandContext, args []string) error { cl, err := dialClient(cctx, &c.Parent.Parent.ClientOptions) if err != nil { @@ -932,35 +1028,39 @@ func (c *TemporalWorkerDeploymentCreateVersionCommand) run(cctx *CommandContext, deploymentName := c.DeploymentName requestID := uuid.NewString() - var cc *computepb.ComputeConfig - if c.AwsLambdaFunctionArn != "" { - detailsPayload, err := awsLambdaProviderDetailsPayload( - c.AwsLambdaFunctionArn, - c.AwsLambdaAssumeRoleArn, - c.AwsLambdaAssumeRoleExternalId, - ) - if err != nil { - return err - } - cc = &computepb.ComputeConfig{ - ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ - "default": { - Provider: &computepb.ComputeProvider{ - Type: "aws-lambda", - Details: detailsPayload, - }, - Scaler: &computepb.ComputeScaler{ - // Hard-coded: no-sync is the only supported algorithm - // in temporal-auto-scaled-workers as of 2026-04-01. - Type: "no-sync", - }, - }, - }, - } - } else { + providerType, detailsPayload, err := computeProviderConfig( + c.AwsLambdaFunctionArn, + c.AwsLambdaAssumeRoleArn, + c.AwsLambdaAssumeRoleExternalId, + c.GcpCloudRunProject, + c.GcpCloudRunRegion, + c.GcpCloudRunWorkerPool, + c.GcpCloudRunServiceAccount, + ) + if err != nil { + return err + } + if providerType == "" { // We do not allow creation of an "empty" WDV. return fmt.Errorf("missing configuration for compute provider") } + scalerType, err := scalerTypeForProvider(providerType) + if err != nil { + return err + } + cc := &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "default": { + Provider: &computepb.ComputeProvider{ + Type: providerType, + Details: detailsPayload, + }, + Scaler: &computepb.ComputeScaler{ + Type: scalerType, + }, + }, + }, + } request := &workflowservice.CreateWorkerDeploymentVersionRequest{ Namespace: ns, DeploymentVersion: &deployment.WorkerDeploymentVersion{ @@ -1005,32 +1105,44 @@ func (c *TemporalWorkerDeploymentUpdateVersionComputeConfigCommand) run(cctx *Co } if c.Remove { - if c.AwsLambdaFunctionArn != "" || c.AwsLambdaAssumeRoleArn != "" || c.AwsLambdaAssumeRoleExternalId != "" { - return fmt.Errorf("--remove cannot be combined with --aws-lambda-function-arn, --aws-lambda-assume-role-arn, or --aws-lambda-assume-role-external-id") + if c.AwsLambdaFunctionArn != "" || c.AwsLambdaAssumeRoleArn != "" || c.AwsLambdaAssumeRoleExternalId != "" || + c.GcpCloudRunProject != "" || c.GcpCloudRunRegion != "" || c.GcpCloudRunWorkerPool != "" || c.GcpCloudRunServiceAccount != "" { + return fmt.Errorf("--remove cannot be combined with --aws-lambda-* or --gcp-cloud-run-* flags") } request.RemoveComputeConfigScalingGroups = []string{"default"} } else { - detailsPayload, err := awsLambdaProviderDetailsPayload( + providerType, detailsPayload, err := computeProviderConfig( c.AwsLambdaFunctionArn, c.AwsLambdaAssumeRoleArn, c.AwsLambdaAssumeRoleExternalId, + c.GcpCloudRunProject, + c.GcpCloudRunRegion, + c.GcpCloudRunWorkerPool, + c.GcpCloudRunServiceAccount, ) if err != nil { return err } + if providerType == "" { + return fmt.Errorf("missing configuration for compute provider") + } + scalerType, err := scalerTypeForProvider(providerType) + if err != nil { + return err + } sg := &computepb.ComputeConfigScalingGroup{ Provider: &computepb.ComputeProvider{ - Type: "aws-lambda", + Type: providerType, Details: detailsPayload, }, Scaler: &computepb.ComputeScaler{ - // Hard-coded: no-sync is the only supported algorithm - // in temporal-auto-scaled-workers as of 2026-04-01. - Type: "no-sync", + Type: scalerType, }, } updatePaths := []string{ + "provider.type", "provider.details", + "scaler.type", } ccScalingGroups := map[string]*computepb.ComputeConfigScalingGroupUpdate{ "default": &computepb.ComputeConfigScalingGroupUpdate{ diff --git a/internal/temporalcli/commands.worker.deployment.internal_test.go b/internal/temporalcli/commands.worker.deployment.internal_test.go new file mode 100644 index 000000000..9f09f9bb9 --- /dev/null +++ b/internal/temporalcli/commands.worker.deployment.internal_test.go @@ -0,0 +1,42 @@ +package temporalcli + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestScalerTypeForProvider(t *testing.T) { + tests := []struct { + name string + provider string + expected string + expectErr bool + }{ + {"aws-lambda is invoke-based -> no-sync", "aws-lambda", "no-sync", false}, + {"gcp-cloud-run is worker-set-based -> rate-based", "gcp-cloud-run", "rate-based", false}, + {"unknown provider errors", "azure-container-apps", "", true}, + {"empty provider errors", "", "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scaler, err := scalerTypeForProvider(tt.provider) + if tt.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.expected, scaler) + }) + } +} + +// Every provider type computeProviderConfig can emit must have an explicit +// scaler mapping; a missing entry makes scalerTypeForProvider error before the +// request is sent, so this guards against forgetting to map a newly-added provider. +func TestScalerTypeByProviderCoversAllProviders(t *testing.T) { + for _, providerType := range []string{"aws-lambda", "gcp-cloud-run"} { + _, ok := scalerTypeByProvider[providerType] + require.Truef(t, ok, "provider %q has no scaler mapping", providerType) + } +} diff --git a/internal/temporalcli/commands.worker.deployment_test.go b/internal/temporalcli/commands.worker.deployment_test.go index b1323532f..d7642ee0f 100644 --- a/internal/temporalcli/commands.worker.deployment_test.go +++ b/internal/temporalcli/commands.worker.deployment_test.go @@ -1311,6 +1311,49 @@ func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_Errors() { s.Error(res.Err) s.ErrorContains(res.Err, "missing required AWS Lambda provider detail: role") + // --gcp-cloud-run-worker-pool requires project, region, and + // service-account; the first missing detail key is reported. + missingGCPProjectBuildID := uuid.NewString() + + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", missingGCPProjectBuildID, + "--gcp-cloud-run-worker-pool", "my-worker-pool", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "missing required GCP Cloud Run provider detail: project") + + // The impersonation target (service-account) is required too. + missingGCPServiceAccountBuildID := uuid.NewString() + + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", missingGCPServiceAccountBuildID, + "--gcp-cloud-run-project", "my-gcp-project", + "--gcp-cloud-run-region", "us-central1", + "--gcp-cloud-run-worker-pool", "my-worker-pool", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "missing required GCP Cloud Run provider detail: service_account") + + // AWS Lambda and GCP Cloud Run providers are mutually exclusive on create. + mixedProvidersBuildID := uuid.NewString() + + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", mixedProvidersBuildID, + "--aws-lambda-function-arn", invokeARN, + "--gcp-cloud-run-worker-pool", "my-worker-pool", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "cannot combine --aws-lambda-* and --gcp-cloud-run-* flags") + // Attempting to update the compute config for a non-existent WDV // should fail. nonExistingBuildID := "non-existing" @@ -1336,6 +1379,32 @@ func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_Errors() { ) s.Error(res.Err) s.ErrorContains(res.Err, "build ID 'non-existing' not found") + + // AWS Lambda and GCP Cloud Run providers are mutually exclusive on update + // too. This is rejected client-side, before the RPC, so the existing + // (lazily-created) build ID is fine to target. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", lazyCreatedBuildID, + "--aws-lambda-function-arn", invokeARN, + "--gcp-cloud-run-worker-pool", "my-worker-pool", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "cannot combine --aws-lambda-* and --gcp-cloud-run-* flags") + + // --remove cannot be combined with GCP Cloud Run flags. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", lazyCreatedBuildID, + "--gcp-cloud-run-worker-pool", "my-worker-pool", + "--remove", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "--remove cannot be combined with") } // TODO(jaypipes): Enable this test when we have a way of ensuring AWS resource @@ -1455,3 +1524,121 @@ func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_LambdaComputeConfi s.NoError(res.Err) s.Contains(res.Stdout.String(), "Successfully removed worker deployment version compute config") } + +// TODO(jaypipes): Enable this test when we have a way of ensuring GCP resource +// fixtures since the CLI test harness uses a real Temporal Server and a real +// Temporal Server validates that any supplied GCP Cloud Run worker pool and +// service account are good (the server impersonates the service account to +// manage the worker pool). +func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_GCPCloudRunComputeConfig() { + s.T().Skip("GCP Cloud Run worker pool and service account fixtures needed.") + deploymentName := uuid.NewString() + taskQueue := uuid.NewString() + + lazyCreatedBuildID := uuid.NewString() + lazyCreatedVer := worker.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildID: lazyCreatedBuildID, + } + + // Create worker with explicit versioning. This will end up creating a + // WorkerDeployment with the specified name. We will then manually create a + // worker deployment version using the `temporal worker deployment + // create-version` command. + w1 := worker.New(s.Client, taskQueue, worker.Options{ + DeploymentOptions: worker.DeploymentOptions{ + UseVersioning: true, + Version: lazyCreatedVer, + }, + }) + + // Register a workflow with explicit Pinned versioning behavior to trigger + // creation of the worker deployment. + w1.RegisterWorkflowWithOptions( + func(ctx workflow.Context, input any) (any, error) { + workflow.GetSignalChannel(ctx, "complete-signal").Receive(ctx, nil) + return nil, nil + }, + workflow.RegisterOptions{ + Name: "TestCreateWorkerDeploymentVersion_GCPCloudRunComputeConfig", + VersioningBehavior: workflow.VersioningBehaviorPinned, + }, + ) + + s.NoError(w1.Start()) + + // Create a WDV with a valid GCP Cloud Run Compute Config specified and + // verify that the compute config provider is displayed in the output of + // `temporal worker deployment describe-version`. + computeConfigBuildID := uuid.NewString() + + project := "my-gcp-project" + region := "us-central1" + workerPool := "my-worker-pool" + serviceAccount := "customer-sa@my-gcp-project.iam.gserviceaccount.com" + + res := s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", computeConfigBuildID, + "--gcp-cloud-run-project", project, + "--gcp-cloud-run-region", region, + "--gcp-cloud-run-worker-pool", workerPool, + "--gcp-cloud-run-service-account", serviceAccount, + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), "Successfully created worker deployment version") + + // Wait for the deployment version to appear + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "describe-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", computeConfigBuildID, + ) + assert.NoError(t, res.Err) + }, 30*time.Second, 100*time.Millisecond) + + // Check that the compute config returned for this WDV reports the + // gcp-cloud-run provider. + res = s.Execute( + "worker", "deployment", "describe-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", computeConfigBuildID, + "--output", "json", + ) + s.NoError(res.Err) + jsonOut := jsonDeploymentVersionInfoType{} + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) + s.NotNil(jsonOut.ComputeConfig, "ComputeConfig should not be nil.") + s.Len(jsonOut.ComputeConfig.ScalingGroups, 1) + s.Equal("gcp-cloud-run", jsonOut.ComputeConfig.ScalingGroups[0].ProviderType) + + // We should be able to update the compute config. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", computeConfigBuildID, + "--gcp-cloud-run-project", project, + "--gcp-cloud-run-region", region, + "--gcp-cloud-run-worker-pool", "updated-worker-pool", + "--gcp-cloud-run-service-account", serviceAccount, + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), "Successfully updated worker deployment version compute config") + + // As well as remove the compute config. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", computeConfigBuildID, + "--remove", + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), "Successfully removed worker deployment version compute config") +} diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index afe54c467..746b3c914 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -1092,7 +1092,21 @@ commands: --aws-lambda-assume-role-arn LambdaAssumeRoleARN \ --aws-lambda-assume-role-external-id LambdaAssumeRoleExternalID ``` - + + Or pass compute provider information for a GCP Cloud Run worker pool + that spawns a Worker in the Worker Deployment: + + ``` + temporal worker deployment create-version \ + --namespace YourNamespaceName \ + --deployment-name YourDeploymentName \ + --build-id YourBuildID \ + --gcp-cloud-run-project YourGCPProject \ + --gcp-cloud-run-region us-central1 \ + --gcp-cloud-run-worker-pool YourWorkerPool \ + --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com + ``` + If a Worker Deployment Version with the supplied BuildID already exists, this command will return an error. @@ -1122,6 +1136,27 @@ commands: with the AWS IAM role specified in --aws-lambda-assume-role-arn has an aws:ExternalId condition that matches the supplied value. Required when --aws-lambda-function-arn is specified. + - name: gcp-cloud-run-project + type: string + description: | + GCP project ID hosting the Cloud Run worker pool. Required when + --gcp-cloud-run-worker-pool is specified. + - name: gcp-cloud-run-region + type: string + description: | + Region of the Cloud Run worker pool. Required when + --gcp-cloud-run-worker-pool is specified. + - name: gcp-cloud-run-worker-pool + type: string + description: | + GCP Cloud Run worker pool name to scale when there are no active + pollers for task queue targets in the Worker Deployment. + - name: gcp-cloud-run-service-account + type: string + description: | + Customer GCP service account the Temporal server impersonates to + manage the Cloud Run worker pool. Required when + --gcp-cloud-run-worker-pool is specified. - name: temporal worker deployment describe-version summary: Show properties of a Worker Deployment Version @@ -1350,6 +1385,18 @@ commands: --aws-lambda-assume-role-arn UpdatedRoleARN ``` + To update the GCP Cloud Run worker pool associated with an existing + Worker Deployment Version: + + ``` + temporal worker deployment update-version-compute-config \ + --deployment-name YourDeploymentName --build-id YourBuildID \ + --gcp-cloud-run-project YourGCPProject \ + --gcp-cloud-run-region us-central1 \ + --gcp-cloud-run-worker-pool UpdatedWorkerPool \ + --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com + ``` + If --remove is specified, the compute configuration for the Worker Deployment Version will be removed: @@ -1386,6 +1433,27 @@ commands: with the AWS IAM role specified in --aws-lambda-assume-role-arn has an aws:ExternalId condition that matches the supplied value. Required when --aws-lambda-function-arn is specified. + - name: gcp-cloud-run-project + type: string + description: | + GCP project ID hosting the Cloud Run worker pool. Required when + --gcp-cloud-run-worker-pool is specified. + - name: gcp-cloud-run-region + type: string + description: | + Region of the Cloud Run worker pool. Required when + --gcp-cloud-run-worker-pool is specified. + - name: gcp-cloud-run-worker-pool + type: string + description: | + GCP Cloud Run worker pool name to scale when there are no active + pollers for task queue targets in the Worker Deployment. + - name: gcp-cloud-run-service-account + type: string + description: | + Customer GCP service account the Temporal server impersonates to + manage the Cloud Run worker pool. Required when + --gcp-cloud-run-worker-pool is specified. - name: remove type: bool description: | From 8a433111488069702d19be6fb1f872cc8e9d2be4 Mon Sep 17 00:00:00 2001 From: Zain Awais <125117323+zainawaisn@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:58:37 -0700 Subject: [PATCH 2/2] Add GCP min/max scaling options for worker deployment version (#1135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Related issues ## What changed? Adds four new (optional) flags `gcp-cloud-run-min-instances`, `gcp-cloud-run-max-instances`, `gcp-cloud-run-initial-instances`, and `gcp-cloud-run-utilization-target` to `create-version` and `update-version-compute-config`. ``` temporal worker deployment create-version \ --deployment-name YourDeployment \ --build-id YourBuildID \ --gcp-cloud-run-project YourProject \ --gcp-cloud-run-region us-central1 \ --gcp-cloud-run-worker-pool YourWorkerPool \ --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com \ --gcp-cloud-run-min-instances 0 \ --gcp-cloud-run-max-instances 10 \ --gcp-cloud-run-initial-instances 5 \ --gcp-cloud-run-utilization-target 0.5 ``` ``` temporal worker deployment update-version-compute-config \ --deployment-name YourDeployment \ --build-id YourBuildID \ --gcp-cloud-run-project YourProject \ --gcp-cloud-run-region us-central1 \ --gcp-cloud-run-worker-pool YourWorkerPool \ --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com \ --gcp-cloud-run-min-instances 0 \ --gcp-cloud-run-max-instances 10 \ --gcp-cloud-run-initial-instances 5 \ --gcp-cloud-run-utilization-target 0.5 ``` ### Changes - ***commands.yaml / commands.gen.go***: four new flags (--gcp-cloud-run-min-instances, --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, --gcp-cloud-run-utilization-target) on create-version and update-version-compute-config, plus help text and usage examples. - ***commands.worker.deployment.go***: a new gcpCloudRunScalerDetails helper turns the flags into the scaling settings sent to the server, and rejects bad input early. All four settings are GCP-only and form a single all-or-none group — setting any one requires all four — with min <= initial <= max and utilization-target a fraction in (0, 1]. Both commands use it, and describe-version now shows the settings back in its JSON and summary output. - ***tests***: unit tests cover the helper's accept/reject cases and the describe-version display; command-level error cases were added to TestCreateWorkerDeploymentVersion_Errors. The end-to-end GCP test stays skipped (needs real GCP resources). ## Checklist **Stability** - [ ] Breaking changes are marked with 💥 in the PR title and release notes - [ ] Changes to JSON output (`-o json` / `-o jsonl`) are treated as breaking changes **Design** - [x] This feature does not depend on Cloud-only APIs or behavior (it works against an OSS server) - [ ] New commands follow `temporal ` structure (e.g. `temporal workflow start`) - [x] New flags are named after the API concept, not the implementation mechanism (good: `--search-attribute`, bad: `--index-field`) - [x] New flags don't duplicate an existing flag that serves the same purpose - [x] New flags do not have short aliases without strong justification - [x] Experimental features are marked with `(Experimental)` in `commands.yaml` **Help text** (see style guide at the top of `commands.yaml`) - [ ] All flags shown in help text and examples are implemented and functional - [x] Summaries use sentence case and have no trailing period - [x] Long descriptions end with a period and include at least one example invocation - [ ] Examples use long flags (`--namespace`, not `-n`), one flag per line - [ ] Placeholder values use `YourXxx` form (`YourWorkflowId`, `YourNamespace`) **Behavior** - [x] Results go to stdout; errors and warnings go to stderr - [x] Error messages are lowercase with no trailing punctuation **Tests** - [ ] Added functional test(s) (`SharedServerSuite`) - [x] Added unit test(s) (`func TestXxx`) where applicable ## Manual tests Creation: ``` ./temporal --profile "ns-cloud-run-test" worker deployment create-version \ --deployment-name "zain-cloud-run-deployment" \ --build-id "1.0" \ --gcp-cloud-run-project "compute-team-sandbox" \ --gcp-cloud-run-region "us-west1" \ --gcp-cloud-run-worker-pool "omes-worker-pool" \ --gcp-cloud-run-service-account "wci-invocation-sa@compute-team-sandbox.iam.gserviceaccount.com" \ --gcp-cloud-run-min-instances 0 \ --gcp-cloud-run-max-instances 10 \ --gcp-cloud-run-initial-instances 5 \ --gcp-cloud-run-utilization-target 0.5 Successfully created worker deployment version ./temporal --profile "ns-cloud-run-test" worker deployment describe-version \ --deployment-name "zain-cloud-run-deployment" \ --build-id "1.0" \ --output json ... "scaler": { "type": "rate-based", "minInstances": 0, "maxInstances": 10, "initialInstances": 5, "utilizationTarget": 0.5 } ... ``` Update existing: ``` ./temporal --profile "ns-cloud-run-test" worker deployment update-version-compute-config \ --deployment-name "zain-cloud-run-deployment" \ --build-id "1.0" \ --gcp-cloud-run-project "compute-team-sandbox" \ --gcp-cloud-run-region "us-west1" \ --gcp-cloud-run-worker-pool "omes-worker-pool" \ --gcp-cloud-run-service-account "wci-invocation-sa@compute-team-sandbox.iam.gserviceaccount.com" \ --gcp-cloud-run-min-instances 0 \ --gcp-cloud-run-max-instances 20 \ --gcp-cloud-run-initial-instances 10 \ --gcp-cloud-run-utilization-target 0.5 Successfully updated worker deployment version compute config ./temporal --profile "ns-cloud-run-test" worker deployment describe-version \ --deployment-name "zain-cloud-run-deployment" \ --build-id "1.0" \ --output json { ... "scaler": { "type": "rate-based", "minInstances": 0, "maxInstances": 20, "initialInstances": 10, "utilizationTarget": 0.5 } ... ``` Error checks: ``` ./temporal --profile "ns-cloud-run-test" worker deployment create-version \ --deployment-name "zain-cloud-run-deployment" \ --build-id "1.0" \ --gcp-cloud-run-project "compute-team-sandbox" \ --gcp-cloud-run-region "us-west1" \ --gcp-cloud-run-worker-pool "omes-worker-pool" \ --gcp-cloud-run-service-account "wci-invocation-sa@compute-team-sandbox.iam.gserviceaccount.com" \ --gcp-cloud-run-min-instances 0 \ --gcp-cloud-run-max-instances 10 \ --gcp-cloud-run-initial-instances 5 Error: --gcp-cloud-run-min-instances, --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and --gcp-cloud-run-utilization-target must be set together ``` ``` ./temporal --profile "ns-cloud-run-test" worker deployment create-version \ --deployment-name "cli-cloud-run-deployment" \ --build-id "1.0" \ --gcp-cloud-run-project "compute-team-sandbox" \ --gcp-cloud-run-region "us-west1" \ --gcp-cloud-run-worker-pool "omes-worker-pool" \ --gcp-cloud-run-service-account "wci-invocation-sa@compute-team-sandbox.iam.gserviceaccount.com" \ --gcp-cloud-run-min-instances 5 \ --gcp-cloud-run-max-instances 10 \ --gcp-cloud-run-initial-instances 0 \ --gcp-cloud-run-utilization-target 0.5 Error: --gcp-cloud-run-initial-instances must be between --gcp-cloud-run-min-instances and --gcp-cloud-run-max-instances ``` ``` ./temporal --profile "ns-cloud-run-test" worker deployment create-version \ --deployment-name my-deploy --build-id v1 \ --aws-lambda-function-arn arn:aws:lambda:us-east-1:123:function:F:1 \ --aws-lambda-assume-role-arn arn:aws:iam::123:role/R \ --aws-lambda-assume-role-external-id x \ --gcp-cloud-run-min-instances 0 \ --gcp-cloud-run-max-instances 10 \ --gcp-cloud-run-initial-instances 5 \ --gcp-cloud-run-utilization-target 0.5 Error: the Cloud Run scaling flags are only valid with --gcp-cloud-run-worker-pool ``` (cherry picked from commit 41debba0ca1e57af2c2886289a8dc55957a2d23b) --- internal/temporalcli/commands.gen.go | 24 +- .../temporalcli/commands.worker.deployment.go | 317 +++++++++++++++--- ...ommands.worker.deployment.internal_test.go | 112 +++++++ .../commands.worker.deployment_test.go | 281 ++++++++++++++++ internal/temporalcli/commands.yaml | 100 +++++- 5 files changed, 789 insertions(+), 45 deletions(-) diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index 3ecadb577..f1456dbdb 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -3311,6 +3311,10 @@ type TemporalWorkerDeploymentCreateVersionCommand struct { GcpCloudRunRegion string GcpCloudRunWorkerPool string GcpCloudRunServiceAccount string + GcpCloudRunMinInstances int + GcpCloudRunMaxInstances int + GcpCloudRunInitialInstances int + GcpCloudRunUtilizationTarget float32 } func NewTemporalWorkerDeploymentCreateVersionCommand(cctx *CommandContext, parent *TemporalWorkerDeploymentCommand) *TemporalWorkerDeploymentCreateVersionCommand { @@ -3320,9 +3324,9 @@ func NewTemporalWorkerDeploymentCreateVersionCommand(cctx *CommandContext, paren s.Command.Use = "create-version [flags]" s.Command.Short = "Create a new Worker Deployment Version" if hasHighlighting { - s.Command.Long = "\nCreate a new Worker Deployment Version:\n\n\x1b[1mtemporal worker deployment create-version [options]\x1b[0m\n\nConfigure a Worker Deployment Version's compute configuration as needed.\nFor example, pass compute provider information for an AWS Lambda function\nthat spawns a Worker in the Worker Deployment:\n\n\x1b[1mtemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --aws-lambda-function-arn LambdaFunctionARN \\\n --aws-lambda-assume-role-arn LambdaAssumeRoleARN \\\n --aws-lambda-assume-role-external-id LambdaAssumeRoleExternalID\x1b[0m\n\nOr pass compute provider information for a GCP Cloud Run worker pool\nthat spawns a Worker in the Worker Deployment:\n\n\x1b[1mtemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool YourWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com\x1b[0m\n\nIf a Worker Deployment Version with the supplied BuildID already exists,\nthis command will return an error.\n\nReturns an error if all compute configuration fields are empty.\n\nNote: This is an experimental feature and may change in the future." + s.Command.Long = "\nCreate a new Worker Deployment Version:\n\n\x1b[1mtemporal worker deployment create-version [options]\x1b[0m\n\nConfigure a Worker Deployment Version's compute configuration as needed.\nFor example, pass compute provider information for an AWS Lambda function\nthat spawns a Worker in the Worker Deployment:\n\n\x1b[1mtemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --aws-lambda-function-arn LambdaFunctionARN \\\n --aws-lambda-assume-role-arn LambdaAssumeRoleARN \\\n --aws-lambda-assume-role-external-id LambdaAssumeRoleExternalID\x1b[0m\n\nOr pass compute provider information for a GCP Cloud Run worker pool\nthat spawns a Worker in the Worker Deployment:\n\n\x1b[1mtemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool YourWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com \\\n --gcp-cloud-run-min-instances 1 \\\n --gcp-cloud-run-max-instances 3 \\\n --gcp-cloud-run-initial-instances 1 \\\n --gcp-cloud-run-utilization-target 0.75\x1b[0m\n\nIf a Worker Deployment Version with the supplied BuildID already exists,\nthis command will return an error.\n\nReturns an error if all compute configuration fields are empty.\n\nNote: This is an experimental feature and may change in the future." } else { - s.Command.Long = "\nCreate a new Worker Deployment Version:\n\n```\ntemporal worker deployment create-version [options]\n```\n\nConfigure a Worker Deployment Version's compute configuration as needed.\nFor example, pass compute provider information for an AWS Lambda function\nthat spawns a Worker in the Worker Deployment:\n\n```\ntemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --aws-lambda-function-arn LambdaFunctionARN \\\n --aws-lambda-assume-role-arn LambdaAssumeRoleARN \\\n --aws-lambda-assume-role-external-id LambdaAssumeRoleExternalID\n```\n\nOr pass compute provider information for a GCP Cloud Run worker pool\nthat spawns a Worker in the Worker Deployment:\n\n```\ntemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool YourWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com\n```\n\nIf a Worker Deployment Version with the supplied BuildID already exists,\nthis command will return an error.\n\nReturns an error if all compute configuration fields are empty.\n\nNote: This is an experimental feature and may change in the future." + s.Command.Long = "\nCreate a new Worker Deployment Version:\n\n```\ntemporal worker deployment create-version [options]\n```\n\nConfigure a Worker Deployment Version's compute configuration as needed.\nFor example, pass compute provider information for an AWS Lambda function\nthat spawns a Worker in the Worker Deployment:\n\n```\ntemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --aws-lambda-function-arn LambdaFunctionARN \\\n --aws-lambda-assume-role-arn LambdaAssumeRoleARN \\\n --aws-lambda-assume-role-external-id LambdaAssumeRoleExternalID\n```\n\nOr pass compute provider information for a GCP Cloud Run worker pool\nthat spawns a Worker in the Worker Deployment:\n\n```\ntemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool YourWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com \\\n --gcp-cloud-run-min-instances 1 \\\n --gcp-cloud-run-max-instances 3 \\\n --gcp-cloud-run-initial-instances 1 \\\n --gcp-cloud-run-utilization-target 0.75\n```\n\nIf a Worker Deployment Version with the supplied BuildID already exists,\nthis command will return an error.\n\nReturns an error if all compute configuration fields are empty.\n\nNote: This is an experimental feature and may change in the future." } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.AwsLambdaFunctionArn, "aws-lambda-function-arn", "", "Qualified (contains version suffix) or unqualified AWS Lambda function ARN to invoke when there are no active pollers for task queue targets in the Worker Deployment.") @@ -3332,6 +3336,10 @@ func NewTemporalWorkerDeploymentCreateVersionCommand(cctx *CommandContext, paren s.Command.Flags().StringVar(&s.GcpCloudRunRegion, "gcp-cloud-run-region", "", "Region of the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified.") s.Command.Flags().StringVar(&s.GcpCloudRunWorkerPool, "gcp-cloud-run-worker-pool", "", "GCP Cloud Run worker pool name to scale when there are no active pollers for task queue targets in the Worker Deployment.") s.Command.Flags().StringVar(&s.GcpCloudRunServiceAccount, "gcp-cloud-run-service-account", "", "Customer GCP service account the Temporal server impersonates to manage the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified.") + s.Command.Flags().IntVar(&s.GcpCloudRunMinInstances, "gcp-cloud-run-min-instances", 0, "Minimum number of Cloud Run worker pool instances the scaler will maintain. Optional, but --gcp-cloud-run-min-instances, --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and --gcp-cloud-run-utilization-target must all be set together. Defaults to 0 when unset. Only valid with --gcp-cloud-run-worker-pool.") + s.Command.Flags().IntVar(&s.GcpCloudRunMaxInstances, "gcp-cloud-run-max-instances", 0, "Maximum number of Cloud Run worker pool instances the scaler may scale up to. Optional, but --gcp-cloud-run-min-instances, --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and --gcp-cloud-run-utilization-target must all be set together. Defaults to 30 when unset. Only valid with --gcp-cloud-run-worker-pool.") + s.Command.Flags().IntVar(&s.GcpCloudRunInitialInstances, "gcp-cloud-run-initial-instances", 0, "Number of Cloud Run worker pool instances the scaler starts with. Optional, but --gcp-cloud-run-min-instances, --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and --gcp-cloud-run-utilization-target must all be set together, and this value must be between the min and max (inclusive). Defaults to 0 when unset. Only valid with --gcp-cloud-run-worker-pool.") + s.Command.Flags().Float32Var(&s.GcpCloudRunUtilizationTarget, "gcp-cloud-run-utilization-target", 0, "Target average worker utilization the scaler aims for, as a fraction in the range (0, 1]. Optional, but --gcp-cloud-run-min-instances, --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and --gcp-cloud-run-utilization-target must all be set together. Lower values keep more spare capacity per worker. Defaults to 0.8 when unset. Only valid with --gcp-cloud-run-worker-pool.") s.DeploymentVersionOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { @@ -3642,6 +3650,10 @@ type TemporalWorkerDeploymentUpdateVersionComputeConfigCommand struct { GcpCloudRunRegion string GcpCloudRunWorkerPool string GcpCloudRunServiceAccount string + GcpCloudRunMinInstances int + GcpCloudRunMaxInstances int + GcpCloudRunInitialInstances int + GcpCloudRunUtilizationTarget float32 Remove bool } @@ -3652,9 +3664,9 @@ func NewTemporalWorkerDeploymentUpdateVersionComputeConfigCommand(cctx *CommandC s.Command.Use = "update-version-compute-config [flags]" s.Command.Short = "Update compute configuration for a Version" if hasHighlighting { - s.Command.Long = "Update compute configuration associated with a Worker Deployment\nVersion.\n\nFor example, to update the AWS Lambda function ARN associated with an\nexisting Worker Deployment Version:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-function-arn UpdatedLambdaFunctionARN\x1b[0m\n\nTo update the AWS IAM role ARN that is assumed by the serverless worker\nmanager associated with an existing Worker Deployment Version:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-assume-role-arn UpdatedRoleARN\x1b[0m\n\nTo update the GCP Cloud Run worker pool associated with an existing\nWorker Deployment Version:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool UpdatedWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com\x1b[0m\n\nIf --remove is specified, the compute configuration for the Worker\nDeployment Version will be removed:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --remove\x1b[0m\n\nIf a Worker Deployment Version with the supplied BuildID does not exist,\nthis command will return an error.\n\nNote: This is an experimental feature and may change in the future." + s.Command.Long = "Update compute configuration associated with a Worker Deployment\nVersion.\n\nFor example, to update the AWS Lambda function ARN associated with an\nexisting Worker Deployment Version:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-function-arn UpdatedLambdaFunctionARN\x1b[0m\n\nTo update the AWS IAM role ARN that is assumed by the serverless worker\nmanager associated with an existing Worker Deployment Version:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-assume-role-arn UpdatedRoleARN\x1b[0m\n\nTo update the GCP Cloud Run worker pool associated with an existing\nWorker Deployment Version:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool UpdatedWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com \\\n --gcp-cloud-run-min-instances 1 \\\n --gcp-cloud-run-max-instances 3 \\\n --gcp-cloud-run-initial-instances 1 \\\n --gcp-cloud-run-utilization-target 0.75\x1b[0m\n\nTo update only the scaling settings on an existing GCP Cloud Run Worker\nDeployment Version, supply the four scaler flags without the provider\nfields (all four must be set together):\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --gcp-cloud-run-min-instances 1 \\\n --gcp-cloud-run-max-instances 3 \\\n --gcp-cloud-run-initial-instances 1 \\\n --gcp-cloud-run-utilization-target 0.75\x1b[0m\n\nProvider fields are only required when changing the compute provider.\nSwitching the provider resets the scaling settings for the new provider.\n\nIf --remove is specified, the compute configuration for the Worker\nDeployment Version will be removed:\n\n\x1b[1m temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --remove\x1b[0m\n\nIf a Worker Deployment Version with the supplied BuildID does not exist,\nthis command will return an error.\n\nNote: This is an experimental feature and may change in the future." } else { - s.Command.Long = "Update compute configuration associated with a Worker Deployment\nVersion.\n\nFor example, to update the AWS Lambda function ARN associated with an\nexisting Worker Deployment Version:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-function-arn UpdatedLambdaFunctionARN\n```\n\nTo update the AWS IAM role ARN that is assumed by the serverless worker\nmanager associated with an existing Worker Deployment Version:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-assume-role-arn UpdatedRoleARN\n```\n\nTo update the GCP Cloud Run worker pool associated with an existing\nWorker Deployment Version:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool UpdatedWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com\n```\n\nIf --remove is specified, the compute configuration for the Worker\nDeployment Version will be removed:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --remove\n```\n\nIf a Worker Deployment Version with the supplied BuildID does not exist,\nthis command will return an error.\n\nNote: This is an experimental feature and may change in the future." + s.Command.Long = "Update compute configuration associated with a Worker Deployment\nVersion.\n\nFor example, to update the AWS Lambda function ARN associated with an\nexisting Worker Deployment Version:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-function-arn UpdatedLambdaFunctionARN\n```\n\nTo update the AWS IAM role ARN that is assumed by the serverless worker\nmanager associated with an existing Worker Deployment Version:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --aws-lambda-assume-role-arn UpdatedRoleARN\n```\n\nTo update the GCP Cloud Run worker pool associated with an existing\nWorker Deployment Version:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --gcp-cloud-run-project YourGCPProject \\\n --gcp-cloud-run-region us-central1 \\\n --gcp-cloud-run-worker-pool UpdatedWorkerPool \\\n --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com \\\n --gcp-cloud-run-min-instances 1 \\\n --gcp-cloud-run-max-instances 3 \\\n --gcp-cloud-run-initial-instances 1 \\\n --gcp-cloud-run-utilization-target 0.75\n```\n\nTo update only the scaling settings on an existing GCP Cloud Run Worker\nDeployment Version, supply the four scaler flags without the provider\nfields (all four must be set together):\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --gcp-cloud-run-min-instances 1 \\\n --gcp-cloud-run-max-instances 3 \\\n --gcp-cloud-run-initial-instances 1 \\\n --gcp-cloud-run-utilization-target 0.75\n```\n\nProvider fields are only required when changing the compute provider.\nSwitching the provider resets the scaling settings for the new provider.\n\nIf --remove is specified, the compute configuration for the Worker\nDeployment Version will be removed:\n\n```\n temporal worker deployment update-version-compute-config \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --remove\n```\n\nIf a Worker Deployment Version with the supplied BuildID does not exist,\nthis command will return an error.\n\nNote: This is an experimental feature and may change in the future." } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.AwsLambdaFunctionArn, "aws-lambda-function-arn", "", "Qualified (contains version suffix) or unqualified AWS Lambda function ARN to invoke when there are no active pollers for task queue targets in the Worker Deployment.") @@ -3664,6 +3676,10 @@ func NewTemporalWorkerDeploymentUpdateVersionComputeConfigCommand(cctx *CommandC s.Command.Flags().StringVar(&s.GcpCloudRunRegion, "gcp-cloud-run-region", "", "Region of the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified.") s.Command.Flags().StringVar(&s.GcpCloudRunWorkerPool, "gcp-cloud-run-worker-pool", "", "GCP Cloud Run worker pool name to scale when there are no active pollers for task queue targets in the Worker Deployment.") s.Command.Flags().StringVar(&s.GcpCloudRunServiceAccount, "gcp-cloud-run-service-account", "", "Customer GCP service account the Temporal server impersonates to manage the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified.") + s.Command.Flags().IntVar(&s.GcpCloudRunMinInstances, "gcp-cloud-run-min-instances", 0, "Minimum number of Cloud Run worker pool instances the scaler will maintain. Optional, but --gcp-cloud-run-min-instances, --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and --gcp-cloud-run-utilization-target must all be set together. If omitted, the version's existing scaling settings are left unchanged. Only valid with --gcp-cloud-run-worker-pool.") + s.Command.Flags().IntVar(&s.GcpCloudRunMaxInstances, "gcp-cloud-run-max-instances", 0, "Maximum number of Cloud Run worker pool instances the scaler may scale up to. Optional, but --gcp-cloud-run-min-instances, --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and --gcp-cloud-run-utilization-target must all be set together. If omitted, the version's existing scaling settings are left unchanged. Only valid with --gcp-cloud-run-worker-pool.") + s.Command.Flags().IntVar(&s.GcpCloudRunInitialInstances, "gcp-cloud-run-initial-instances", 0, "Number of Cloud Run worker pool instances the scaler starts with. Optional, but --gcp-cloud-run-min-instances, --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and --gcp-cloud-run-utilization-target must all be set together, and this value must be between the min and max (inclusive). If omitted, the version's existing scaling settings are left unchanged. Only valid with --gcp-cloud-run-worker-pool.") + s.Command.Flags().Float32Var(&s.GcpCloudRunUtilizationTarget, "gcp-cloud-run-utilization-target", 0, "Target average worker utilization the scaler aims for, as a fraction in the range (0, 1]. Optional, but --gcp-cloud-run-min-instances, --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and --gcp-cloud-run-utilization-target must all be set together. Lower values keep more spare capacity per worker. If omitted, the version's existing scaling settings are left unchanged. Only valid with --gcp-cloud-run-worker-pool.") s.Command.Flags().BoolVar(&s.Remove, "remove", false, "Removes any compute configuration associated with this Worker Deployment Version.") s.DeploymentVersionOptions.BuildFlags(s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { diff --git a/internal/temporalcli/commands.worker.deployment.go b/internal/temporalcli/commands.worker.deployment.go index a32d749ee..2f788ae2f 100644 --- a/internal/temporalcli/commands.worker.deployment.go +++ b/internal/temporalcli/commands.worker.deployment.go @@ -136,7 +136,11 @@ type formattedComputeConfigProvider struct { } type formattedComputeConfigScaler struct { - Type string `json:"type"` + Type string `json:"type"` + MinInstances *int64 `json:"minInstances,omitempty"` + MaxInstances *int64 `json:"maxInstances,omitempty"` + InitialInstances *int64 `json:"initialInstances,omitempty"` + UtilizationTarget *float64 `json:"utilizationTarget,omitempty"` } func drainageStatusToStr(drainage client.WorkerDeploymentVersionDrainageStatus) (string, error) { @@ -380,6 +384,84 @@ func formatDrainageInfoProto(drainageInfo *deploymentpb.VersionDrainageInfo) (fo }, nil } +// Config keys for the WCI rate-based scaler, shared by the build path +// (gcpCloudRunScalerDetails) and the read path (decodeScalerSettings) so the two +// never drift. Mirrors: +// https://github.com/temporalio/temporal-auto-scaled-workers/blob/main/wci/workflow/scaling_algorithm/rate_based.go +const ( + scalerKeyMinCount = "min_count" + scalerKeyMaxCount = "max_count" + scalerKeyInitialCount = "initial_count" + scalerKeyUtilizationTarget = "utilization_target" +) + +// scalerCountFromMap reads an integer worker-count value from a decoded scaler +// details map. The default data converter round-trips JSON numbers as float64, +// so that case is handled alongside the native int types. +func scalerCountFromMap(m map[string]any, key string) (int64, bool) { + switch n := m[key].(type) { + case float64: + return int64(n), true + case int64: + return n, true + case int: + return int64(n), true + default: + return 0, false + } +} + +// scalerFloatFromMap reads a fractional value (e.g. utilization_target) from a +// decoded scaler details map. JSON numbers decode as float64. +func scalerFloatFromMap(m map[string]any, key string) (float64, bool) { + switch n := m[key].(type) { + case float64: + return n, true + case float32: + return float64(n), true + default: + return 0, false + } +} + +// scalerSettings holds the rate-based scaler settings surfaced for display. Each +// field is nil when the corresponding key is absent from the scaler details. +type scalerSettings struct { + minInstances *int64 + maxInstances *int64 + initialInstances *int64 + utilizationTarget *float64 +} + +// decodeScalerSettings extracts the rate-based scaler settings from a +// ComputeScaler's details payload for display. Best-effort: returns zero-value +// (all-nil) settings when the scaler, its details, or a key is absent, or when +// the payload cannot be decoded, so read paths never fail on an unexpected shape. +func decodeScalerSettings(s *computepb.ComputeScaler) scalerSettings { + var out scalerSettings + details := s.GetDetails() + if details == nil { + return out + } + var m map[string]any + if err := converter.GetDefaultDataConverter().FromPayload(details, &m); err != nil { + return out + } + if v, ok := scalerCountFromMap(m, scalerKeyMinCount); ok { + out.minInstances = &v + } + if v, ok := scalerCountFromMap(m, scalerKeyMaxCount); ok { + out.maxInstances = &v + } + if v, ok := scalerCountFromMap(m, scalerKeyInitialCount); ok { + out.initialInstances = &v + } + if v, ok := scalerFloatFromMap(m, scalerKeyUtilizationTarget); ok { + out.utilizationTarget = &v + } + return out +} + func formatComputeConfigProto(cc *computepb.ComputeConfig) *formattedComputeConfig { if cc == nil { return nil @@ -401,10 +483,14 @@ func formatComputeConfigProto(cc *computepb.ComputeConfig) *formattedComputeConf Type: p.GetType(), } } - if p != nil { - sg.Scaler = &formattedComputeConfigScaler{ - Type: s.GetType(), - } + if s != nil { + fs := &formattedComputeConfigScaler{Type: s.GetType()} + set := decodeScalerSettings(s) + fs.MinInstances = set.minInstances + fs.MaxInstances = set.maxInstances + fs.InitialInstances = set.initialInstances + fs.UtilizationTarget = set.utilizationTarget + sg.Scaler = fs } sgs[name] = sg } @@ -446,18 +532,37 @@ func computeConfigSummaryStr(cc *computepb.ComputeConfig) string { if cc == nil { return "" } - providers := []string{} + summaries := []string{} for _, sg := range cc.GetScalingGroups() { p := sg.GetProvider() if p == nil { continue } - pt := p.GetType() - if !slices.Contains(providers, pt) { - providers = append(providers, pt) + summary := p.GetType() + // Append whichever scaler settings are present so the one-line summary + // reflects the configured limits (ordered min, initial, max, utilization). + set := decodeScalerSettings(sg.GetScaler()) + parts := []string{} + if set.minInstances != nil { + parts = append(parts, fmt.Sprintf("min %d", *set.minInstances)) + } + if set.initialInstances != nil { + parts = append(parts, fmt.Sprintf("initial %d", *set.initialInstances)) + } + if set.maxInstances != nil { + parts = append(parts, fmt.Sprintf("max %d", *set.maxInstances)) + } + if set.utilizationTarget != nil { + parts = append(parts, fmt.Sprintf("utilization %g", *set.utilizationTarget)) + } + if len(parts) > 0 { + summary = fmt.Sprintf("%s (%s)", summary, strings.Join(parts, ", ")) + } + if !slices.Contains(summaries, summary) { + summaries = append(summaries, summary) } } - return strings.Join(providers, ",") + return strings.Join(summaries, ",") } // printWorkerDeploymentVersionInfoProto prints worker deployment version info from proto types. @@ -1015,6 +1120,99 @@ func scalerTypeForProvider(providerType string) (string, error) { return "", fmt.Errorf("no scaler mapping for compute provider %q", providerType) } +// gcpCloudRunScalerDetails builds the ComputeScaler.Details payload from the GCP +// Cloud Run scaling flags. It carries two independent groups of rate-based scaler +// settings: +// - the instance-count group (min_count/max_count/initial_count), which is +// all-or-none and must satisfy min <= initial <= max, and +// - utilization_target, a standalone fraction in (0, 1]. +// +// The *Set booleans come from cobra's Flags().Changed, so an omitted flag stays +// distinct from an explicit 0. Returns a nil payload when nothing is set, leaving +// WCI's defaults (min 0, max 30, initial 0, utilization_target 0.8) in effect. +// Every setting is GCP Cloud Run only; any use with another provider is rejected. +// Config keys mirror the WCI rate-based scaler: +// https://github.com/temporalio/temporal-auto-scaled-workers/blob/main/wci/workflow/scaling_algorithm/rate_based.go +// gcpScalerFlags holds the GCP Cloud Run scaling flag values together with +// whether each was actually set (from cobra's Flags().Changed). Pairing each +// value with its Set bool keeps an omitted flag distinct from an explicit 0 and +// removes the positional-argument risk of passing the raw values around. +type gcpScalerFlags struct { + min int + minSet bool + max int + maxSet bool + initial int + initialSet bool + utilization float32 + utilizationSet bool +} + +func (f gcpScalerFlags) anySet() bool { + return f.minSet || f.maxSet || f.initialSet || f.utilizationSet +} + +func (f gcpScalerFlags) allSet() bool { + return f.minSet && f.maxSet && f.initialSet && f.utilizationSet +} + +// gcpCloudRunScalerDetails builds the ComputeScaler.Details payload from the GCP +// Cloud Run scaling flags (min/max/initial instance counts and utilization +// target). The four form a single all-or-none group: setting any one requires +// all four. That keeps the min<=initial<=max relationship self-contained and +// avoids comparing an explicit value against WCI's default for an unset sibling. +// Returns a nil payload when nothing is set, leaving WCI's defaults (min 0, +// max 30, initial 0, utilization_target 0.8) in effect. Every setting is GCP +// Cloud Run only; any use with another provider is rejected. +func gcpCloudRunScalerDetails(providerType string, f gcpScalerFlags) (*commonpb.Payload, error) { + if !f.anySet() { + return nil, nil + } + // These are GCP Cloud Run (rate-based) knobs only. Reject on any use with + // another provider so the GCP-only nature is explicit, regardless of value. + if providerType != "gcp-cloud-run" { + return nil, fmt.Errorf("the Cloud Run scaling flags are only valid with --gcp-cloud-run-worker-pool") + } + if !f.allSet() { + return nil, fmt.Errorf("--gcp-cloud-run-min-instances, --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and --gcp-cloud-run-utilization-target must be set together") + } + if f.min < 0 { + return nil, fmt.Errorf("--gcp-cloud-run-min-instances cannot be negative") + } + if f.max < 1 { + return nil, fmt.Errorf("--gcp-cloud-run-max-instances must be at least 1") + } + if f.min > f.max { + return nil, fmt.Errorf("--gcp-cloud-run-min-instances cannot exceed --gcp-cloud-run-max-instances") + } + if f.initial < f.min || f.initial > f.max { + return nil, fmt.Errorf("--gcp-cloud-run-initial-instances must be between --gcp-cloud-run-min-instances and --gcp-cloud-run-max-instances") + } + // The (0, 1] range is intrinsic to utilization_target's meaning, not a + // default that could drift, so mirroring the check here is safe. + if f.utilization <= 0 || f.utilization > 1 { + return nil, fmt.Errorf("--gcp-cloud-run-utilization-target must be greater than 0 and at most 1") + } + details := map[string]any{ + scalerKeyMinCount: f.min, + scalerKeyMaxCount: f.max, + scalerKeyInitialCount: f.initial, + scalerKeyUtilizationTarget: f.utilization, + } + dc := converter.GetDefaultDataConverter() + return dc.ToPayload(&details) +} + +func (c *TemporalWorkerDeploymentCreateVersionCommand) gcpScalerFlags() gcpScalerFlags { + f := c.Command.Flags() + return gcpScalerFlags{ + min: c.GcpCloudRunMinInstances, minSet: f.Changed("gcp-cloud-run-min-instances"), + max: c.GcpCloudRunMaxInstances, maxSet: f.Changed("gcp-cloud-run-max-instances"), + initial: c.GcpCloudRunInitialInstances, initialSet: f.Changed("gcp-cloud-run-initial-instances"), + utilization: c.GcpCloudRunUtilizationTarget, utilizationSet: f.Changed("gcp-cloud-run-utilization-target"), + } +} + func (c *TemporalWorkerDeploymentCreateVersionCommand) run(cctx *CommandContext, args []string) error { cl, err := dialClient(cctx, &c.Parent.Parent.ClientOptions) if err != nil { @@ -1048,6 +1246,10 @@ func (c *TemporalWorkerDeploymentCreateVersionCommand) run(cctx *CommandContext, if err != nil { return err } + scalerDetails, err := gcpCloudRunScalerDetails(providerType, c.gcpScalerFlags()) + if err != nil { + return err + } cc := &computepb.ComputeConfig{ ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ "default": { @@ -1056,7 +1258,8 @@ func (c *TemporalWorkerDeploymentCreateVersionCommand) run(cctx *CommandContext, Details: detailsPayload, }, Scaler: &computepb.ComputeScaler{ - Type: scalerType, + Type: scalerType, + Details: scalerDetails, }, }, }, @@ -1081,6 +1284,16 @@ func (c *TemporalWorkerDeploymentCreateVersionCommand) run(cctx *CommandContext, return nil } +func (c *TemporalWorkerDeploymentUpdateVersionComputeConfigCommand) gcpScalerFlags() gcpScalerFlags { + f := c.Command.Flags() + return gcpScalerFlags{ + min: c.GcpCloudRunMinInstances, minSet: f.Changed("gcp-cloud-run-min-instances"), + max: c.GcpCloudRunMaxInstances, maxSet: f.Changed("gcp-cloud-run-max-instances"), + initial: c.GcpCloudRunInitialInstances, initialSet: f.Changed("gcp-cloud-run-initial-instances"), + utilization: c.GcpCloudRunUtilizationTarget, utilizationSet: f.Changed("gcp-cloud-run-utilization-target"), + } +} + func (c *TemporalWorkerDeploymentUpdateVersionComputeConfigCommand) run(cctx *CommandContext, args []string) error { cl, err := dialClient(cctx, &c.Parent.Parent.ClientOptions) if err != nil { @@ -1106,7 +1319,8 @@ func (c *TemporalWorkerDeploymentUpdateVersionComputeConfigCommand) run(cctx *Co if c.Remove { if c.AwsLambdaFunctionArn != "" || c.AwsLambdaAssumeRoleArn != "" || c.AwsLambdaAssumeRoleExternalId != "" || - c.GcpCloudRunProject != "" || c.GcpCloudRunRegion != "" || c.GcpCloudRunWorkerPool != "" || c.GcpCloudRunServiceAccount != "" { + c.GcpCloudRunProject != "" || c.GcpCloudRunRegion != "" || c.GcpCloudRunWorkerPool != "" || c.GcpCloudRunServiceAccount != "" || + c.gcpScalerFlags().anySet() { return fmt.Errorf("--remove cannot be combined with --aws-lambda-* or --gcp-cloud-run-* flags") } request.RemoveComputeConfigScalingGroups = []string{"default"} @@ -1123,37 +1337,62 @@ func (c *TemporalWorkerDeploymentUpdateVersionComputeConfigCommand) run(cctx *Co if err != nil { return err } - if providerType == "" { - return fmt.Errorf("missing configuration for compute provider") - } - scalerType, err := scalerTypeForProvider(providerType) - if err != nil { - return err - } - sg := &computepb.ComputeConfigScalingGroup{ - Provider: &computepb.ComputeProvider{ - Type: providerType, - Details: detailsPayload, - }, - Scaler: &computepb.ComputeScaler{ - Type: scalerType, - }, - } - updatePaths := []string{ - "provider.type", - "provider.details", - "scaler.type", + scalerFlags := c.gcpScalerFlags() + + var sg *computepb.ComputeConfigScalingGroup + var updatePaths []string + switch { + case providerType != "": + // Provider (re)configuration: rebuild the provider and scaler type, + // and keep scaler.details consistent with the (possibly new) type. + scalerType, err := scalerTypeForProvider(providerType) + if err != nil { + return err + } + scalerDetails, err := gcpCloudRunScalerDetails(providerType, scalerFlags) + if err != nil { + return err + } + sg = &computepb.ComputeConfigScalingGroup{ + Provider: &computepb.ComputeProvider{ + Type: providerType, + Details: detailsPayload, + }, + Scaler: &computepb.ComputeScaler{ + Type: scalerType, + Details: scalerDetails, + }, + } + updatePaths = []string{"provider.type", "provider.details", "scaler.type"} + // scaler.details must stay consistent with the scaler type. A + // non-GCP scaler (no-sync) can't carry the rate-based details, so + // clear them when switching away from GCP; for GCP, overwrite only + // when new settings were supplied, otherwise preserve existing ones. + if providerType != "gcp-cloud-run" || scalerDetails != nil { + updatePaths = append(updatePaths, "scaler.details") + } + case scalerFlags.anySet(): + // Scaler-only update: change scaler.details without touching the + // provider or scaler type. Targets the version's existing GCP Cloud + // Run scaler; the server validates against the real config. + scalerDetails, err := gcpCloudRunScalerDetails("gcp-cloud-run", scalerFlags) + if err != nil { + return err + } + sg = &computepb.ComputeConfigScalingGroup{ + Scaler: &computepb.ComputeScaler{Details: scalerDetails}, + } + updatePaths = []string{"scaler.details"} + default: + return fmt.Errorf("no compute configuration provided to update") } - ccScalingGroups := map[string]*computepb.ComputeConfigScalingGroupUpdate{ - "default": &computepb.ComputeConfigScalingGroupUpdate{ + + request.ComputeConfigScalingGroups = map[string]*computepb.ComputeConfigScalingGroupUpdate{ + "default": { ScalingGroup: sg, - UpdateMask: &fieldmaskpb.FieldMask{ - Paths: updatePaths, - }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: updatePaths}, }, } - request.ComputeConfigScalingGroups = ccScalingGroups - } _, err = cl.WorkflowService().UpdateWorkerDeploymentVersionComputeConfig(cctx, request) diff --git a/internal/temporalcli/commands.worker.deployment.internal_test.go b/internal/temporalcli/commands.worker.deployment.internal_test.go index 9f09f9bb9..9db39ad77 100644 --- a/internal/temporalcli/commands.worker.deployment.internal_test.go +++ b/internal/temporalcli/commands.worker.deployment.internal_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" + computepb "go.temporal.io/api/compute/v1" + "go.temporal.io/sdk/converter" ) func TestScalerTypeForProvider(t *testing.T) { @@ -40,3 +42,113 @@ func TestScalerTypeByProviderCoversAllProviders(t *testing.T) { require.Truef(t, ok, "provider %q has no scaler mapping", providerType) } } + +func TestGCPCloudRunScalerDetails(t *testing.T) { + // Nothing set -> nil payload so WCI defaults apply (min 0, max 30, + // initial 0, utilization_target 0.8). + p, err := gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{}) + require.NoError(t, err) + require.Nil(t, p) + + // Any scaler flag alongside a non-GCP provider is rejected. Covers both an + // instance-count flag and the utilization flag. + _, err = gcpCloudRunScalerDetails("aws-lambda", gcpScalerFlags{minSet: true}) + require.ErrorContains(t, err, "only valid with --gcp-cloud-run-worker-pool") + _, err = gcpCloudRunScalerDetails("aws-lambda", gcpScalerFlags{utilization: 0.5, utilizationSet: true}) + require.ErrorContains(t, err, "only valid with --gcp-cloud-run-worker-pool") + + // All four settings are one all-or-none group: any partial set is rejected. + _, err = gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{min: 5, minSet: true}) + require.ErrorContains(t, err, "must be set together") + _, err = gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{utilization: 0.5, utilizationSet: true}) // utilization alone + require.ErrorContains(t, err, "must be set together") + _, err = gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{ // trio set, utilization missing + min: 1, minSet: true, max: 3, maxSet: true, initial: 2, initialSet: true, + }) + require.ErrorContains(t, err, "must be set together") + + // Value checks, with all four set so the group check passes first. + _, err = gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{min: -1, minSet: true, max: 3, maxSet: true, initial: 0, initialSet: true, utilization: 0.5, utilizationSet: true}) + require.ErrorContains(t, err, "cannot be negative") + _, err = gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{min: 0, minSet: true, max: 0, maxSet: true, initial: 0, initialSet: true, utilization: 0.5, utilizationSet: true}) + require.ErrorContains(t, err, "--gcp-cloud-run-max-instances must be at least 1") + _, err = gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{min: 5, minSet: true, max: 3, maxSet: true, initial: 4, initialSet: true, utilization: 0.5, utilizationSet: true}) + require.ErrorContains(t, err, "cannot exceed") + _, err = gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{min: 2, minSet: true, max: 10, maxSet: true, initial: 15, initialSet: true, utilization: 0.5, utilizationSet: true}) + require.ErrorContains(t, err, "must be between") + _, err = gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{min: 0, minSet: true, max: 10, maxSet: true, initial: 5, initialSet: true, utilization: 0, utilizationSet: true}) + require.ErrorContains(t, err, "must be greater than 0 and at most 1") + _, err = gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{min: 0, minSet: true, max: 10, maxSet: true, initial: 5, initialSet: true, utilization: 1.5, utilizationSet: true}) + require.ErrorContains(t, err, "must be greater than 0 and at most 1") + + // All four set and valid -> payload decodes to the WCI rate-based keys. + // JSON round-trips numbers as float64; WCI handles that on read. + p, err = gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{min: 1, minSet: true, max: 10, maxSet: true, initial: 5, initialSet: true, utilization: 0.5, utilizationSet: true}) + require.NoError(t, err) + require.NotNil(t, p) + var details map[string]any + require.NoError(t, converter.GetDefaultDataConverter().FromPayload(p, &details)) + require.Equal(t, float64(1), details[scalerKeyMinCount]) + require.Equal(t, float64(10), details[scalerKeyMaxCount]) + require.Equal(t, float64(5), details[scalerKeyInitialCount]) + require.Equal(t, float64(0.5), details[scalerKeyUtilizationTarget]) +} + +func TestFormatComputeConfigProto_ScalerBounds(t *testing.T) { + // Build the scaler details the same way the run methods do. + scalerDetails, err := gcpCloudRunScalerDetails("gcp-cloud-run", gcpScalerFlags{ + min: 0, minSet: true, + max: 10, maxSet: true, + initial: 5, initialSet: true, + utilization: 0.75, utilizationSet: true, + }) + require.NoError(t, err) + require.NotNil(t, scalerDetails) + + cc := &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "default": { + Provider: &computepb.ComputeProvider{Type: "gcp-cloud-run"}, + Scaler: &computepb.ComputeScaler{Type: "rate-based", Details: scalerDetails}, + }, + }, + } + + // JSON/structured path surfaces min, max, initial, and utilization. + formatted := formatComputeConfigProto(cc) + require.NotNil(t, formatted) + sg, ok := formatted.ScalingGroups["default"] + require.True(t, ok) + require.NotNil(t, sg.Scaler) + require.Equal(t, "rate-based", sg.Scaler.Type) + require.NotNil(t, sg.Scaler.MinInstances) + require.NotNil(t, sg.Scaler.MaxInstances) + require.NotNil(t, sg.Scaler.InitialInstances) + require.NotNil(t, sg.Scaler.UtilizationTarget) + require.Equal(t, int64(0), *sg.Scaler.MinInstances) + require.Equal(t, int64(10), *sg.Scaler.MaxInstances) + require.Equal(t, int64(5), *sg.Scaler.InitialInstances) + require.Equal(t, float64(0.75), *sg.Scaler.UtilizationTarget) + + // Human-readable summary reflects the settings (min, initial, max, utilization). + require.Equal(t, "gcp-cloud-run (min 0, initial 5, max 10, utilization 0.75)", computeConfigSummaryStr(cc)) + + // Without scaler details, the settings are nil and the summary is just the + // provider (guards against printing zeroed-out values). + ccNoBounds := &computepb.ComputeConfig{ + ScalingGroups: map[string]*computepb.ComputeConfigScalingGroup{ + "default": { + Provider: &computepb.ComputeProvider{Type: "gcp-cloud-run"}, + Scaler: &computepb.ComputeScaler{Type: "rate-based"}, + }, + }, + } + formatted = formatComputeConfigProto(ccNoBounds) + sg = formatted.ScalingGroups["default"] + require.NotNil(t, sg.Scaler) + require.Nil(t, sg.Scaler.MinInstances) + require.Nil(t, sg.Scaler.MaxInstances) + require.Nil(t, sg.Scaler.InitialInstances) + require.Nil(t, sg.Scaler.UtilizationTarget) + require.Equal(t, "gcp-cloud-run", computeConfigSummaryStr(ccNoBounds)) +} diff --git a/internal/temporalcli/commands.worker.deployment_test.go b/internal/temporalcli/commands.worker.deployment_test.go index d7642ee0f..e1a7115b4 100644 --- a/internal/temporalcli/commands.worker.deployment_test.go +++ b/internal/temporalcli/commands.worker.deployment_test.go @@ -1,11 +1,13 @@ package temporalcli_test import ( + "context" "encoding/base64" "encoding/json" "fmt" "sort" "strings" + "sync" "time" "github.com/google/uuid" @@ -15,9 +17,11 @@ import ( enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" + "go.temporal.io/sdk/converter" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/worker" "go.temporal.io/sdk/workflow" + "google.golang.org/grpc" ) type jsonVersionSummariesRowType struct { @@ -1405,6 +1409,283 @@ func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_Errors() { ) s.Error(res.Err) s.ErrorContains(res.Err, "--remove cannot be combined with") + + // --remove cannot be combined with the scaler settings flags either; a lone + // scaler flag alongside --remove is contradictory and rejected client-side. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", lazyCreatedBuildID, + "--gcp-cloud-run-min-instances", "5", + "--remove", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "--remove cannot be combined with") + + // Instance settings are GCP Cloud Run (rate-based) knobs only; setting them + // alongside a (fully specified) AWS Lambda provider is rejected client-side. + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--aws-lambda-function-arn", invokeARN, + "--aws-lambda-assume-role-arn", assumeRoleARN, + "--aws-lambda-assume-role-external-id", assumeRoleExternalID, + "--gcp-cloud-run-min-instances", "1", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "only valid with --gcp-cloud-run-worker-pool") + + // A lone flag is rejected: all four scaling settings must be set together. + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--gcp-cloud-run-project", "my-gcp-project", + "--gcp-cloud-run-region", "us-central1", + "--gcp-cloud-run-worker-pool", "my-worker-pool", + "--gcp-cloud-run-service-account", "customer-sa@my-gcp-project.iam.gserviceaccount.com", + "--gcp-cloud-run-min-instances", "32", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "must be set together") + + // The instance counts without utilization-target are also rejected: + // utilization-target is part of the same all-or-none group. + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--gcp-cloud-run-project", "my-gcp-project", + "--gcp-cloud-run-region", "us-central1", + "--gcp-cloud-run-worker-pool", "my-worker-pool", + "--gcp-cloud-run-service-account", "customer-sa@my-gcp-project.iam.gserviceaccount.com", + "--gcp-cloud-run-min-instances", "1", + "--gcp-cloud-run-max-instances", "3", + "--gcp-cloud-run-initial-instances", "2", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "must be set together") + + // min cannot exceed max (all four set so the group check passes first). + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--gcp-cloud-run-project", "my-gcp-project", + "--gcp-cloud-run-region", "us-central1", + "--gcp-cloud-run-worker-pool", "my-worker-pool", + "--gcp-cloud-run-service-account", "customer-sa@my-gcp-project.iam.gserviceaccount.com", + "--gcp-cloud-run-min-instances", "5", + "--gcp-cloud-run-max-instances", "3", + "--gcp-cloud-run-initial-instances", "4", + "--gcp-cloud-run-utilization-target", "0.5", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "cannot exceed") + + // An explicit max of 0 is rejected (WCI requires at least 1). + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--gcp-cloud-run-project", "my-gcp-project", + "--gcp-cloud-run-region", "us-central1", + "--gcp-cloud-run-worker-pool", "my-worker-pool", + "--gcp-cloud-run-service-account", "customer-sa@my-gcp-project.iam.gserviceaccount.com", + "--gcp-cloud-run-min-instances", "0", + "--gcp-cloud-run-max-instances", "0", + "--gcp-cloud-run-initial-instances", "0", + "--gcp-cloud-run-utilization-target", "0.5", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "--gcp-cloud-run-max-instances must be at least 1") + + // initial outside [min, max] is rejected. + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--gcp-cloud-run-project", "my-gcp-project", + "--gcp-cloud-run-region", "us-central1", + "--gcp-cloud-run-worker-pool", "my-worker-pool", + "--gcp-cloud-run-service-account", "customer-sa@my-gcp-project.iam.gserviceaccount.com", + "--gcp-cloud-run-min-instances", "2", + "--gcp-cloud-run-max-instances", "10", + "--gcp-cloud-run-initial-instances", "15", + "--gcp-cloud-run-utilization-target", "0.5", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "must be between") + + // utilization-target must be a fraction in (0, 1] (all four set so the + // group check passes first). + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--gcp-cloud-run-project", "my-gcp-project", + "--gcp-cloud-run-region", "us-central1", + "--gcp-cloud-run-worker-pool", "my-worker-pool", + "--gcp-cloud-run-service-account", "customer-sa@my-gcp-project.iam.gserviceaccount.com", + "--gcp-cloud-run-min-instances", "0", + "--gcp-cloud-run-max-instances", "10", + "--gcp-cloud-run-initial-instances", "5", + "--gcp-cloud-run-utilization-target", "1.5", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "must be greater than 0 and at most 1") + + // utilization-target is GCP Cloud Run only; rejected alongside AWS Lambda. + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--aws-lambda-function-arn", invokeARN, + "--aws-lambda-assume-role-arn", assumeRoleARN, + "--aws-lambda-assume-role-external-id", assumeRoleExternalID, + "--gcp-cloud-run-utilization-target", "0.75", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "only valid with --gcp-cloud-run-worker-pool") + + // Instance settings with AWS Lambda are also rejected on update, before the RPC. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", lazyCreatedBuildID, + "--aws-lambda-function-arn", invokeARN, + "--aws-lambda-assume-role-arn", assumeRoleARN, + "--aws-lambda-assume-role-external-id", assumeRoleExternalID, + "--gcp-cloud-run-max-instances", "3", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "only valid with --gcp-cloud-run-worker-pool") +} + +// TestUpdateWorkerDeploymentVersionComputeConfig_UpdateModes verifies the shape +// of the update request for each mode. It captures the outbound gRPC request and +// short-circuits it, so the assertions don't need a real (GCP-backed) version on +// the server. +func (s *SharedServerSuite) TestUpdateWorkerDeploymentVersionComputeConfig_UpdateModes() { + var mu sync.Mutex + var captured *workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest + s.CommandHarness.Options.AdditionalClientGRPCDialOptions = append( + s.CommandHarness.Options.AdditionalClientGRPCDialOptions, + grpc.WithChainUnaryInterceptor(func( + ctx context.Context, method string, req, reply any, + cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, + ) error { + if r, ok := req.(*workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest); ok { + mu.Lock() + captured = r + mu.Unlock() + return nil // capture only; do not forward to the server + } + return invoker(ctx, method, req, reply, cc, opts...) + }), + ) + takeCaptured := func() *workflowservice.UpdateWorkerDeploymentVersionComputeConfigRequest { + mu.Lock() + defer mu.Unlock() + r := captured + captured = nil + return r + } + + deploymentName := uuid.NewString() + buildID := "1.0" + serviceAccount := "customer-sa@my-gcp-project.iam.gserviceaccount.com" + + // Scaler-only update (no provider flags): the mask is just scaler.details, + // no provider is sent, and all four settings are carried. + res := s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, "--build-id", buildID, + "--gcp-cloud-run-min-instances", "0", + "--gcp-cloud-run-max-instances", "10", + "--gcp-cloud-run-initial-instances", "5", + "--gcp-cloud-run-utilization-target", "0.5", + ) + s.NoError(res.Err) + req := takeCaptured() + s.NotNil(req) + sg := req.GetComputeConfigScalingGroups()["default"] + s.NotNil(sg) + s.Equal([]string{"scaler.details"}, sg.GetUpdateMask().GetPaths()) + s.Nil(sg.GetScalingGroup().GetProvider()) + var details map[string]any + s.NoError(converter.GetDefaultDataConverter().FromPayload(sg.GetScalingGroup().GetScaler().GetDetails(), &details)) + s.Equal(float64(0), details["min_count"]) + s.Equal(float64(10), details["max_count"]) + s.Equal(float64(5), details["initial_count"]) + s.Equal(float64(0.5), details["utilization_target"]) + + // Switching to AWS Lambda clears the (rate-based) scaler.details so they + // don't linger under the no-sync scaler. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, "--build-id", buildID, + "--aws-lambda-function-arn", "arn:aws:lambda:us-east-1:123:function:F:1", + "--aws-lambda-assume-role-arn", "arn:aws:iam::123:role/R", + "--aws-lambda-assume-role-external-id", "x", + ) + s.NoError(res.Err) + req = takeCaptured() + s.NotNil(req) + sg = req.GetComputeConfigScalingGroups()["default"] + s.Contains(sg.GetUpdateMask().GetPaths(), "scaler.details") + s.Equal("aws-lambda", sg.GetScalingGroup().GetProvider().GetType()) + s.Nil(sg.GetScalingGroup().GetScaler().GetDetails()) + + // Updating only GCP provider fields (no scaler flags) leaves scaler.details + // out of the mask, so existing bounds are preserved. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, "--build-id", buildID, + "--gcp-cloud-run-project", "my-gcp-project", + "--gcp-cloud-run-region", "us-central1", + "--gcp-cloud-run-worker-pool", "my-worker-pool", + "--gcp-cloud-run-service-account", serviceAccount, + ) + s.NoError(res.Err) + req = takeCaptured() + s.NotNil(req) + sg = req.GetComputeConfigScalingGroups()["default"] + s.NotContains(sg.GetUpdateMask().GetPaths(), "scaler.details") + s.Equal("gcp-cloud-run", sg.GetScalingGroup().GetProvider().GetType()) + + // A scaler-only update still requires all four flags together. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, "--build-id", buildID, + "--gcp-cloud-run-min-instances", "5", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "must be set together") + + // Nothing to update at all is rejected. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, "--build-id", buildID, + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "no compute configuration provided to update") } // TODO(jaypipes): Enable this test when we have a way of ensuring AWS resource diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index 746b3c914..7ca3f075f 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -1104,7 +1104,11 @@ commands: --gcp-cloud-run-project YourGCPProject \ --gcp-cloud-run-region us-central1 \ --gcp-cloud-run-worker-pool YourWorkerPool \ - --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com + --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com \ + --gcp-cloud-run-min-instances 1 \ + --gcp-cloud-run-max-instances 3 \ + --gcp-cloud-run-initial-instances 1 \ + --gcp-cloud-run-utilization-target 0.75 ``` If a Worker Deployment Version with the supplied BuildID already exists, @@ -1157,6 +1161,40 @@ commands: Customer GCP service account the Temporal server impersonates to manage the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified. + - name: gcp-cloud-run-min-instances + type: int + description: | + Minimum number of Cloud Run worker pool instances the scaler will + maintain. Optional, but --gcp-cloud-run-min-instances, + --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and + --gcp-cloud-run-utilization-target must all be set together. Defaults + to 0 when unset. Only valid with --gcp-cloud-run-worker-pool. + - name: gcp-cloud-run-max-instances + type: int + description: | + Maximum number of Cloud Run worker pool instances the scaler may + scale up to. Optional, but --gcp-cloud-run-min-instances, + --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and + --gcp-cloud-run-utilization-target must all be set together. Defaults + to 30 when unset. Only valid with --gcp-cloud-run-worker-pool. + - name: gcp-cloud-run-initial-instances + type: int + description: | + Number of Cloud Run worker pool instances the scaler starts with. + Optional, but --gcp-cloud-run-min-instances, + --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and + --gcp-cloud-run-utilization-target must all be set together, and this + value must be between the min and max (inclusive). Defaults to 0 when + unset. Only valid with --gcp-cloud-run-worker-pool. + - name: gcp-cloud-run-utilization-target + type: float + description: | + Target average worker utilization the scaler aims for, as a fraction + in the range (0, 1]. Optional, but --gcp-cloud-run-min-instances, + --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and + --gcp-cloud-run-utilization-target must all be set together. Lower + values keep more spare capacity per worker. Defaults to 0.8 when + unset. Only valid with --gcp-cloud-run-worker-pool. - name: temporal worker deployment describe-version summary: Show properties of a Worker Deployment Version @@ -1394,9 +1432,29 @@ commands: --gcp-cloud-run-project YourGCPProject \ --gcp-cloud-run-region us-central1 \ --gcp-cloud-run-worker-pool UpdatedWorkerPool \ - --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com + --gcp-cloud-run-service-account customer-sa@proj.iam.gserviceaccount.com \ + --gcp-cloud-run-min-instances 1 \ + --gcp-cloud-run-max-instances 3 \ + --gcp-cloud-run-initial-instances 1 \ + --gcp-cloud-run-utilization-target 0.75 ``` + To update only the scaling settings on an existing GCP Cloud Run Worker + Deployment Version, supply the four scaler flags without the provider + fields (all four must be set together): + + ``` + temporal worker deployment update-version-compute-config \ + --deployment-name YourDeploymentName --build-id YourBuildID \ + --gcp-cloud-run-min-instances 1 \ + --gcp-cloud-run-max-instances 3 \ + --gcp-cloud-run-initial-instances 1 \ + --gcp-cloud-run-utilization-target 0.75 + ``` + + Provider fields are only required when changing the compute provider. + Switching the provider resets the scaling settings for the new provider. + If --remove is specified, the compute configuration for the Worker Deployment Version will be removed: @@ -1454,6 +1512,44 @@ commands: Customer GCP service account the Temporal server impersonates to manage the Cloud Run worker pool. Required when --gcp-cloud-run-worker-pool is specified. + - name: gcp-cloud-run-min-instances + type: int + description: | + Minimum number of Cloud Run worker pool instances the scaler will + maintain. Optional, but --gcp-cloud-run-min-instances, + --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and + --gcp-cloud-run-utilization-target must all be set together. If + omitted, the version's existing scaling settings are left unchanged. + Only valid with --gcp-cloud-run-worker-pool. + - name: gcp-cloud-run-max-instances + type: int + description: | + Maximum number of Cloud Run worker pool instances the scaler may + scale up to. Optional, but --gcp-cloud-run-min-instances, + --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and + --gcp-cloud-run-utilization-target must all be set together. If + omitted, the version's existing scaling settings are left unchanged. + Only valid with --gcp-cloud-run-worker-pool. + - name: gcp-cloud-run-initial-instances + type: int + description: | + Number of Cloud Run worker pool instances the scaler starts with. + Optional, but --gcp-cloud-run-min-instances, + --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and + --gcp-cloud-run-utilization-target must all be set together, and this + value must be between the min and max (inclusive). If omitted, the + version's existing scaling settings are left unchanged. Only valid + with --gcp-cloud-run-worker-pool. + - name: gcp-cloud-run-utilization-target + type: float + description: | + Target average worker utilization the scaler aims for, as a fraction + in the range (0, 1]. Optional, but --gcp-cloud-run-min-instances, + --gcp-cloud-run-max-instances, --gcp-cloud-run-initial-instances, and + --gcp-cloud-run-utilization-target must all be set together. Lower + values keep more spare capacity per worker. If omitted, the version's + existing scaling settings are left unchanged. Only valid with + --gcp-cloud-run-worker-pool. - name: remove type: bool description: |