From 4dbf4b52c69a1791ee4bf9c8b2f93adb8c5adbff Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Fri, 16 Oct 2026 02:01:29 -0700 Subject: [PATCH] adds modal integration for serverless worker --- internal/temporalcli/commands.gen.go | 28 +++- .../temporalcli/commands.worker.deployment.go | 148 ++++++++++++++---- ...ommands.worker.deployment.internal_test.go | 59 ++++++- .../commands.worker.deployment_test.go | 141 ++++++++++++++++- internal/temporalcli/commands.yaml | 78 +++++++++ 5 files changed, 412 insertions(+), 42 deletions(-) diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index 3f625ab05..cc8c2619b 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -3769,6 +3769,11 @@ type TemporalWorkerDeploymentCreateVersionCommand struct { AwsAgentcoreAssumeRoleArn string AwsAgentcoreAssumeRoleExternalId string AwsAgentcoreSkipRoleAndExternalId bool + ModalAppName string + ModalFunctionName string + ModalEnvironment string + ModalWorkspaceId string + ModalVersion string GcpCloudRunProject string GcpCloudRunRegion string GcpCloudRunWorkerPool string @@ -3787,9 +3792,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 an AWS Bedrock Agentcore Runtime\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-agentcore-endpoint-arn AgentcoreRuntimeEndpointARN \\\n --aws-agentcore-assume-role-arn AgentcoreAssumeRoleARN \\\n --aws-agentcore-assume-role-external-id AgentcoreAssumeRoleExternalID\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 \\\n --gcp-cloud-run-scale-down-stabilization-duration 5m\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 an AWS Bedrock Agentcore Runtime\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-agentcore-endpoint-arn AgentcoreRuntimeEndpointARN \\\n --aws-agentcore-assume-role-arn AgentcoreAssumeRoleARN \\\n --aws-agentcore-assume-role-external-id AgentcoreAssumeRoleExternalID\x1b[0m\n\nOr pass a deployed Modal application and function that spawns a Worker in\nthe Worker Deployment:\n\n\x1b[1mtemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --modal-app-name YourModalApp \\\n --modal-function-name YourModalFunction \\\n --modal-environment main\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 \\\n --gcp-cloud-run-scale-down-stabilization-duration 5m\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 an AWS Bedrock Agentcore Runtime\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-agentcore-endpoint-arn AgentcoreRuntimeEndpointARN \\\n --aws-agentcore-assume-role-arn AgentcoreAssumeRoleARN \\\n --aws-agentcore-assume-role-external-id AgentcoreAssumeRoleExternalID\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 --gcp-cloud-run-scale-down-stabilization-duration 5m\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 an AWS Bedrock Agentcore Runtime\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-agentcore-endpoint-arn AgentcoreRuntimeEndpointARN \\\n --aws-agentcore-assume-role-arn AgentcoreAssumeRoleARN \\\n --aws-agentcore-assume-role-external-id AgentcoreAssumeRoleExternalID\n```\n\nOr pass a deployed Modal application and function that spawns a Worker in\nthe Worker Deployment:\n\n```\ntemporal worker deployment create-version \\\n --namespace YourNamespaceName \\\n --deployment-name YourDeploymentName \\\n --build-id YourBuildID \\\n --modal-app-name YourModalApp \\\n --modal-function-name YourModalFunction \\\n --modal-environment main\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 --gcp-cloud-run-scale-down-stabilization-duration 5m\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.") @@ -3800,6 +3805,11 @@ func NewTemporalWorkerDeploymentCreateVersionCommand(cctx *CommandContext, paren s.Command.Flags().StringVar(&s.AwsAgentcoreAssumeRoleArn, "aws-agentcore-assume-role-arn", "", "AWS IAM role ARN that the Temporal server will assume when invoking the Agentcore Runtime that spawns a new Worker in this Worker Deployment Version. Required when --aws-agentcore-endpoint-arn is specified, and must be omitted when --aws-agentcore-skip-role-and-external-id is passed.") s.Command.Flags().StringVar(&s.AwsAgentcoreAssumeRoleExternalId, "aws-agentcore-assume-role-external-id", "", "Temporal server will enforce that the AWS IAM trust policy associated with the AWS IAM role specified in --aws-agentcore-assume-role-arn has an aws:ExternalId condition that matches the supplied value. Required when --aws-agentcore-endpoint-arn is specified, and must be omitted when --aws-agentcore-skip-role-and-external-id is passed.") s.Command.Flags().BoolVar(&s.AwsAgentcoreSkipRoleAndExternalId, "aws-agentcore-skip-role-and-external-id", false, "When --aws-agentcore-endpoint-arn is specified, --aws-agentcore-assume-role-arn and --aws-agentcore-assume-role-external-id are required unless this flag is passed, in which case both must be omitted.") + s.Command.Flags().StringVar(&s.ModalAppName, "modal-app-name", "", "Name of the deployed Modal application containing the Worker function. Required when configuring a Modal compute provider.") + s.Command.Flags().StringVar(&s.ModalFunctionName, "modal-function-name", "", "Name of the Modal function to invoke when there are no active pollers for task queue targets in the Worker Deployment. Required when configuring a Modal compute provider.") + s.Command.Flags().StringVar(&s.ModalEnvironment, "modal-environment", "", "Modal environment containing the deployed application. Required when configuring a Modal compute provider.") + s.Command.Flags().StringVar(&s.ModalWorkspaceId, "modal-workspace-id", "", "Optional Modal workspace ID used to select the workspace's credential grant. Omit when the server uses ambient Modal credentials.") + s.Command.Flags().StringVar(&s.ModalVersion, "modal-version", "", "Optional deployed Modal application version, expressed as a non-negative integer. Omit to use the application's current deployed version.") 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.") @@ -4121,6 +4131,11 @@ type TemporalWorkerDeploymentUpdateVersionComputeConfigCommand struct { AwsAgentcoreAssumeRoleArn string AwsAgentcoreAssumeRoleExternalId string AwsAgentcoreSkipRoleAndExternalId bool + ModalAppName string + ModalFunctionName string + ModalEnvironment string + ModalWorkspaceId string + ModalVersion string GcpCloudRunProject string GcpCloudRunRegion string GcpCloudRunWorkerPool string @@ -4140,9 +4155,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 AWS Bedrock Agentcore Runtime endpoint 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-agentcore-endpoint-arn UpdatedAgentcoreRuntimeEndpointARN \\\n --aws-agentcore-assume-role-arn UpdatedRoleARN \\\n --aws-agentcore-assume-role-external-id UpdatedExternalID\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 \\\n --gcp-cloud-run-scale-down-stabilization-duration 5m\x1b[0m\n\nTo update only the scaling settings on an existing GCP Cloud Run Worker\nDeployment Version, supply the five scaler flags without the provider\nfields (all five 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 \\\n --gcp-cloud-run-scale-down-stabilization-duration 5m\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." + 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 AWS Bedrock Agentcore Runtime endpoint 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-agentcore-endpoint-arn UpdatedAgentcoreRuntimeEndpointARN \\\n --aws-agentcore-assume-role-arn UpdatedRoleARN \\\n --aws-agentcore-assume-role-external-id UpdatedExternalID\x1b[0m\n\nTo update the Modal application and function 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 --modal-app-name UpdatedModalApp \\\n --modal-function-name UpdatedModalFunction \\\n --modal-environment main\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 \\\n --gcp-cloud-run-scale-down-stabilization-duration 5m\x1b[0m\n\nTo update only the scaling settings on an existing GCP Cloud Run Worker\nDeployment Version, supply the five scaler flags without the provider\nfields (all five 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 \\\n --gcp-cloud-run-scale-down-stabilization-duration 5m\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 AWS Bedrock Agentcore Runtime endpoint 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-agentcore-endpoint-arn UpdatedAgentcoreRuntimeEndpointARN \\\n --aws-agentcore-assume-role-arn UpdatedRoleARN \\\n --aws-agentcore-assume-role-external-id UpdatedExternalID\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 --gcp-cloud-run-scale-down-stabilization-duration 5m\n```\n\nTo update only the scaling settings on an existing GCP Cloud Run Worker\nDeployment Version, supply the five scaler flags without the provider\nfields (all five 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 --gcp-cloud-run-scale-down-stabilization-duration 5m\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.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 AWS Bedrock Agentcore Runtime endpoint 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-agentcore-endpoint-arn UpdatedAgentcoreRuntimeEndpointARN \\\n --aws-agentcore-assume-role-arn UpdatedRoleARN \\\n --aws-agentcore-assume-role-external-id UpdatedExternalID\n```\n\nTo update the Modal application and function 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 --modal-app-name UpdatedModalApp \\\n --modal-function-name UpdatedModalFunction \\\n --modal-environment main\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 --gcp-cloud-run-scale-down-stabilization-duration 5m\n```\n\nTo update only the scaling settings on an existing GCP Cloud Run Worker\nDeployment Version, supply the five scaler flags without the provider\nfields (all five 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 --gcp-cloud-run-scale-down-stabilization-duration 5m\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.") @@ -4153,6 +4168,11 @@ func NewTemporalWorkerDeploymentUpdateVersionComputeConfigCommand(cctx *CommandC s.Command.Flags().StringVar(&s.AwsAgentcoreAssumeRoleArn, "aws-agentcore-assume-role-arn", "", "AWS IAM role ARN that the Temporal server will assume when invoking the Agentcore Runtime that spawns a new Worker in this Worker Deployment Version. Required when --aws-agentcore-endpoint-arn is specified, and must be omitted when --aws-agentcore-skip-role-and-external-id is passed.") s.Command.Flags().StringVar(&s.AwsAgentcoreAssumeRoleExternalId, "aws-agentcore-assume-role-external-id", "", "Temporal server will enforce that the AWS IAM trust policy associated with the AWS IAM role specified in --aws-agentcore-assume-role-arn has an aws:ExternalId condition that matches the supplied value. Required when --aws-agentcore-endpoint-arn is specified, and must be omitted when --aws-agentcore-skip-role-and-external-id is passed.") s.Command.Flags().BoolVar(&s.AwsAgentcoreSkipRoleAndExternalId, "aws-agentcore-skip-role-and-external-id", false, "When --aws-agentcore-endpoint-arn is specified, --aws-agentcore-assume-role-arn and --aws-agentcore-assume-role-external-id are required unless this flag is passed, in which case both must be omitted.") + s.Command.Flags().StringVar(&s.ModalAppName, "modal-app-name", "", "Name of the deployed Modal application containing the Worker function. Required when configuring a Modal compute provider.") + s.Command.Flags().StringVar(&s.ModalFunctionName, "modal-function-name", "", "Name of the Modal function to invoke when there are no active pollers for task queue targets in the Worker Deployment. Required when configuring a Modal compute provider.") + s.Command.Flags().StringVar(&s.ModalEnvironment, "modal-environment", "", "Modal environment containing the deployed application. Required when configuring a Modal compute provider.") + s.Command.Flags().StringVar(&s.ModalWorkspaceId, "modal-workspace-id", "", "Optional Modal workspace ID used to select the workspace's credential grant. Omit when the server uses ambient Modal credentials.") + s.Command.Flags().StringVar(&s.ModalVersion, "modal-version", "", "Optional deployed Modal application version, expressed as a non-negative integer. Omit to use the application's current deployed version.") 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.") diff --git a/internal/temporalcli/commands.worker.deployment.go b/internal/temporalcli/commands.worker.deployment.go index 6dece0b35..a5ea95ddf 100644 --- a/internal/temporalcli/commands.worker.deployment.go +++ b/internal/temporalcli/commands.worker.deployment.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "slices" + "strconv" "strings" "time" @@ -1133,6 +1134,52 @@ func gcpCloudRunProviderDetailsPayload( return dc.ToPayload(&providerDetails) } +func validateModalProviderDetails(details map[string]any) error { + for _, key := range []string{"app_name", "function_name", "environment"} { + if v, ok := details[key].(string); !ok || v == "" { + return fmt.Errorf("missing required Modal provider detail: %s", key) + } + } + if rawVersion, ok := details["version"]; ok { + version, ok := rawVersion.(string) + parsed, err := strconv.Atoi(version) + if !ok || err != nil || parsed < 0 { + return fmt.Errorf("Modal provider detail %q must be a non-negative integer string", "version") + } + } + return nil +} + +// modalProviderDetailsPayload returns the encoded Payload expected by WCI's +// invoke-based Modal provider. The environment is part of the compute resource +// identity and must be explicit so provider updates cannot silently change it. +func modalProviderDetailsPayload( + appName string, + functionName string, + environment string, + workspaceID string, + version string, +) (*commonpb.Payload, error) { + // Map keys from temporal-auto-scaled-workers: + // https://github.com/temporalio/temporal-auto-scaled-workers/blob/324950cf843e560ff0c6531070bb1d4e139d5000/wci/workflow/compute_provider/modal.go#L18-L24 + providerDetails := map[string]any{ + "app_name": appName, + "function_name": functionName, + "environment": environment, + } + if workspaceID != "" { + providerDetails["workspace_id"] = workspaceID + } + if version != "" { + providerDetails["version"] = version + } + if err := validateModalProviderDetails(providerDetails); err != nil { + return nil, err + } + dc := converter.GetDefaultDataConverter() + return dc.ToPayload(&providerDetails) +} + // ComputeConfigArgs holds ComputeConfig specific args along with helpers for distinguishing between desired compute // provider type ComputeConfigArgs struct { @@ -1148,6 +1195,11 @@ type ComputeConfigArgs struct { gcpCloudRunRegion string gcpCloudRunWorkerPool string gcpCloudRunServiceAccount string + modalAppName string + modalFunctionName string + modalEnvironment string + modalWorkspaceID string + modalVersion string } func (c *ComputeConfigArgs) hasAwsLambdaArgs() bool { @@ -1171,24 +1223,34 @@ func (c *ComputeConfigArgs) hasGcpCloudRunArgs() bool { c.gcpCloudRunServiceAccount != "" } +func (c *ComputeConfigArgs) hasModalArgs() bool { + return c.modalAppName != "" || + c.modalFunctionName != "" || + c.modalEnvironment != "" || + c.modalWorkspaceID != "" || + c.modalVersion != "" +} + // computeProviderConfig selects the single compute provider for a Worker // Deployment Version's "default" scaling group from the command's flags. It // enforces that flags for different providers are not mixed, then dispatches on // the trigger flag (--aws-lambda-function-arn / --aws-agentcore-endpoint-arn / -// --gcp-cloud-run-worker-pool). Returns an empty providerType when no provider -// flags are set, leaving the "no configuration" decision to the caller. +// --gcp-cloud-run-worker-pool / --modal-app-name). Returns an empty providerType +// when no provider flags are set, leaving the "no configuration" decision to +// the caller. func computeProviderConfig(c *ComputeConfigArgs) (providerType string, detailsPayload *commonpb.Payload, err error) { awsLambdaSet := c.hasAwsLambdaArgs() awsAgentcoreSet := c.hasAwsAgentcoreArgs() gcpSet := c.hasGcpCloudRunArgs() + modalSet := c.hasModalArgs() setCount := 0 - for _, set := range []bool{awsLambdaSet, awsAgentcoreSet, gcpSet} { + for _, set := range []bool{awsLambdaSet, awsAgentcoreSet, gcpSet, modalSet} { if set { setCount++ } } if setCount > 1 { - return "", nil, fmt.Errorf("cannot combine --aws-lambda-*, --aws-agentcore-*, and --gcp-cloud-run-* flags; a Worker Deployment Version supports a single compute provider") + return "", nil, fmt.Errorf("cannot combine --aws-lambda-*, --aws-agentcore-*, --gcp-cloud-run-*, and --modal-* flags; a Worker Deployment Version supports a single compute provider") } switch { @@ -1216,19 +1278,29 @@ func computeProviderConfig(c *ComputeConfigArgs) (providerType string, detailsPa c.gcpCloudRunServiceAccount, ) return "gcp-cloud-run", p, err + case c.hasModalArgs(): + p, err := modalProviderDetailsPayload( + c.modalAppName, + c.modalFunctionName, + c.modalEnvironment, + c.modalWorkspaceID, + c.modalVersion, + ) + return "modal", 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. +// compatible with its launch strategy in temporal-auto-scaled-workers. Invoke +// providers use "no-sync"; worker-set providers use "rate-based". WCI rejects +// an incompatible pairing at CreateWorkerDeploymentVersion. var scalerTypeByProvider = map[string]string{ "aws-lambda": "no-sync", "aws-agentcore": "no-sync", "gcp-cloud-run": "rate-based", + "modal": "no-sync", } // scalerTypeForProvider returns the scaling algorithm for the given provider, @@ -1350,18 +1422,23 @@ func (c *TemporalWorkerDeploymentCreateVersionCommand) run(cctx *CommandContext, requestID := uuid.NewString() providerType, detailsPayload, err := computeProviderConfig(&ComputeConfigArgs{ - awsLambdaFunctionArn: c.AwsLambdaFunctionArn, - awsLambdaAssumeRoleArn: c.AwsLambdaAssumeRoleArn, - awsLambdaAssumeRoleExternalId: c.AwsLambdaAssumeRoleExternalId, - awsLambdaSkipRoleAndExternalId: c.AwsLambdaSkipRoleAndExternalId, - awsAgentcoreEndpointArn: c.AwsAgentcoreEndpointArn, - awsAgentcoreAssumeRoleArn: c.AwsAgentcoreAssumeRoleArn, - awsAgentcoreAssumeRoleExternalId: c.AwsAgentcoreAssumeRoleExternalId, + awsLambdaFunctionArn: c.AwsLambdaFunctionArn, + awsLambdaAssumeRoleArn: c.AwsLambdaAssumeRoleArn, + awsLambdaAssumeRoleExternalId: c.AwsLambdaAssumeRoleExternalId, + awsLambdaSkipRoleAndExternalId: c.AwsLambdaSkipRoleAndExternalId, + awsAgentcoreEndpointArn: c.AwsAgentcoreEndpointArn, + awsAgentcoreAssumeRoleArn: c.AwsAgentcoreAssumeRoleArn, + awsAgentcoreAssumeRoleExternalId: c.AwsAgentcoreAssumeRoleExternalId, awsAgentcoreSkipRoleAndExternalId: c.AwsAgentcoreSkipRoleAndExternalId, - gcpCloudRunProject: c.GcpCloudRunProject, - gcpCloudRunRegion: c.GcpCloudRunRegion, - gcpCloudRunWorkerPool: c.GcpCloudRunWorkerPool, - gcpCloudRunServiceAccount: c.GcpCloudRunServiceAccount, + gcpCloudRunProject: c.GcpCloudRunProject, + gcpCloudRunRegion: c.GcpCloudRunRegion, + gcpCloudRunWorkerPool: c.GcpCloudRunWorkerPool, + gcpCloudRunServiceAccount: c.GcpCloudRunServiceAccount, + modalAppName: c.ModalAppName, + modalFunctionName: c.ModalFunctionName, + modalEnvironment: c.ModalEnvironment, + modalWorkspaceID: c.ModalWorkspaceId, + modalVersion: c.ModalVersion, }) if err != nil { return err @@ -1447,24 +1524,29 @@ func (c *TemporalWorkerDeploymentUpdateVersionComputeConfigCommand) run(cctx *Co } computeConfigArgs := &ComputeConfigArgs{ - awsLambdaFunctionArn: c.AwsLambdaFunctionArn, - awsLambdaAssumeRoleArn: c.AwsLambdaAssumeRoleArn, - awsLambdaAssumeRoleExternalId: c.AwsLambdaAssumeRoleExternalId, - awsLambdaSkipRoleAndExternalId: c.AwsLambdaSkipRoleAndExternalId, - awsAgentcoreEndpointArn: c.AwsAgentcoreEndpointArn, - awsAgentcoreAssumeRoleArn: c.AwsAgentcoreAssumeRoleArn, - awsAgentcoreAssumeRoleExternalId: c.AwsAgentcoreAssumeRoleExternalId, - awsAgentcoreSkipRoleAndExternalId: c.AwsAgentcoreSkipRoleAndExternalId, - gcpCloudRunProject: c.GcpCloudRunProject, - gcpCloudRunRegion: c.GcpCloudRunRegion, - gcpCloudRunWorkerPool: c.GcpCloudRunWorkerPool, - gcpCloudRunServiceAccount: c.GcpCloudRunServiceAccount, - } + awsLambdaFunctionArn: c.AwsLambdaFunctionArn, + awsLambdaAssumeRoleArn: c.AwsLambdaAssumeRoleArn, + awsLambdaAssumeRoleExternalId: c.AwsLambdaAssumeRoleExternalId, + awsLambdaSkipRoleAndExternalId: c.AwsLambdaSkipRoleAndExternalId, + awsAgentcoreEndpointArn: c.AwsAgentcoreEndpointArn, + awsAgentcoreAssumeRoleArn: c.AwsAgentcoreAssumeRoleArn, + awsAgentcoreAssumeRoleExternalId: c.AwsAgentcoreAssumeRoleExternalId, + awsAgentcoreSkipRoleAndExternalId: c.AwsAgentcoreSkipRoleAndExternalId, + gcpCloudRunProject: c.GcpCloudRunProject, + gcpCloudRunRegion: c.GcpCloudRunRegion, + gcpCloudRunWorkerPool: c.GcpCloudRunWorkerPool, + gcpCloudRunServiceAccount: c.GcpCloudRunServiceAccount, + modalAppName: c.ModalAppName, + modalFunctionName: c.ModalFunctionName, + modalEnvironment: c.ModalEnvironment, + modalWorkspaceID: c.ModalWorkspaceId, + modalVersion: c.ModalVersion, + } if c.Remove { - if computeConfigArgs.hasAwsLambdaArgs() || computeConfigArgs.hasAwsAgentcoreArgs() || computeConfigArgs.hasGcpCloudRunArgs() || + if computeConfigArgs.hasAwsLambdaArgs() || computeConfigArgs.hasAwsAgentcoreArgs() || computeConfigArgs.hasGcpCloudRunArgs() || computeConfigArgs.hasModalArgs() || c.gcpScalerFlags().anySet() { - return fmt.Errorf("--remove cannot be combined with --aws-lambda-*, --aws-agentcore-*, or --gcp-cloud-run-* flags") + return fmt.Errorf("--remove cannot be combined with --aws-lambda-*, --aws-agentcore-*, --gcp-cloud-run-*, or --modal-* flags") } request.RemoveComputeConfigScalingGroups = []string{"default"} } else { diff --git a/internal/temporalcli/commands.worker.deployment.internal_test.go b/internal/temporalcli/commands.worker.deployment.internal_test.go index f25603819..84a1a62c6 100644 --- a/internal/temporalcli/commands.worker.deployment.internal_test.go +++ b/internal/temporalcli/commands.worker.deployment.internal_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" computepb "go.temporal.io/api/compute/v1" "go.temporal.io/sdk/converter" ) @@ -18,6 +19,7 @@ func TestScalerTypeForProvider(t *testing.T) { }{ {"aws-lambda is invoke-based -> no-sync", "aws-lambda", "no-sync", false}, {"aws-agentcore is invoke-based -> no-sync", "aws-agentcore", "no-sync", false}, + {"modal is invoke-based -> no-sync", "modal", "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}, @@ -39,12 +41,67 @@ func TestScalerTypeForProvider(t *testing.T) { // 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", "aws-agentcore", "gcp-cloud-run"} { + for _, providerType := range []string{"aws-lambda", "aws-agentcore", "gcp-cloud-run", "modal"} { _, ok := scalerTypeByProvider[providerType] require.Truef(t, ok, "provider %q has no scaler mapping", providerType) } } +func TestModalProviderDetailsPayload(t *testing.T) { + decode := func(t *testing.T, payload *commonpb.Payload) map[string]any { + t.Helper() + var details map[string]any + require.NoError(t, converter.GetDefaultDataConverter().FromPayload(payload, &details)) + return details + } + + p, err := modalProviderDetailsPayload("payments", "worker", "main", "", "") + require.NoError(t, err) + details := decode(t, p) + require.Equal(t, "payments", details["app_name"]) + require.Equal(t, "worker", details["function_name"]) + require.Equal(t, "main", details["environment"]) + require.NotContains(t, details, "workspace_id") + require.NotContains(t, details, "version") + + p, err = modalProviderDetailsPayload("payments", "worker", "staging", "ws-123", "42") + require.NoError(t, err) + details = decode(t, p) + require.Equal(t, "staging", details["environment"]) + require.Equal(t, "ws-123", details["workspace_id"]) + require.Equal(t, "42", details["version"]) + + _, err = modalProviderDetailsPayload("", "worker", "main", "", "") + require.ErrorContains(t, err, "missing required Modal provider detail: app_name") + _, err = modalProviderDetailsPayload("payments", "", "main", "", "") + require.ErrorContains(t, err, "missing required Modal provider detail: function_name") + _, err = modalProviderDetailsPayload("payments", "worker", "", "", "") + require.ErrorContains(t, err, "missing required Modal provider detail: environment") + _, err = modalProviderDetailsPayload("payments", "worker", "main", "", "-1") + require.ErrorContains(t, err, "must be a non-negative integer string") + _, err = modalProviderDetailsPayload("payments", "worker", "main", "", "latest") + require.ErrorContains(t, err, "must be a non-negative integer string") +} + +func TestComputeProviderConfig_Modal(t *testing.T) { + providerType, payload, err := computeProviderConfig(&ComputeConfigArgs{ + modalAppName: "payments", + modalFunctionName: "worker", + modalEnvironment: "main", + }) + require.NoError(t, err) + require.Equal(t, "modal", providerType) + require.NotNil(t, payload) + + _, _, err = computeProviderConfig(&ComputeConfigArgs{ + awsLambdaFunctionArn: "arn:aws:lambda:us-east-1:123:function:worker", + modalAppName: "payments", + modalFunctionName: "worker", + modalEnvironment: "main", + }) + require.ErrorContains(t, err, "cannot combine") +} + func TestGCPCloudRunScalerDetails(t *testing.T) { // A fully-set, valid group; each case clones this and overrides one field so // the all-or-none check passes and the case isolates a single value check. diff --git a/internal/temporalcli/commands.worker.deployment_test.go b/internal/temporalcli/commands.worker.deployment_test.go index db9c62661..8710b07df 100644 --- a/internal/temporalcli/commands.worker.deployment_test.go +++ b/internal/temporalcli/commands.worker.deployment_test.go @@ -1402,7 +1402,7 @@ func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_Errors() { "--gcp-cloud-run-worker-pool", "my-worker-pool", ) s.Error(res.Err) - s.ErrorContains(res.Err, "cannot combine --aws-lambda-*, --aws-agentcore-*, and --gcp-cloud-run-* flags") + s.ErrorContains(res.Err, "cannot combine --aws-lambda-*, --aws-agentcore-*, --gcp-cloud-run-*, and --modal-* flags") // AWS Agentcore and Lambda providers are mutually exclusive on create. res = s.Execute( @@ -1415,7 +1415,7 @@ func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_Errors() { "--aws-lambda-assume-role-external-id", assumeRoleExternalID, ) s.Error(res.Err) - s.ErrorContains(res.Err, "cannot combine --aws-lambda-*, --aws-agentcore-*, and --gcp-cloud-run-* flags") + s.ErrorContains(res.Err, "cannot combine --aws-lambda-*, --aws-agentcore-*, --gcp-cloud-run-*, and --modal-* flags") // --gcp-cloud-run-worker-pool requires project, region, and // service-account; the first missing detail key is reported. @@ -1446,6 +1446,51 @@ func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_Errors() { s.Error(res.Err) s.ErrorContains(res.Err, "missing required GCP Cloud Run provider detail: service_account") + // Modal requires the deployed App, Function, and environment. + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--modal-app-name", "payments", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "missing required Modal provider detail: function_name") + + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--modal-function-name", "worker", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "missing required Modal provider detail: app_name") + + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--modal-app-name", "payments", + "--modal-function-name", "worker", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "missing required Modal provider detail: environment") + + res = s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", uuid.NewString(), + "--modal-app-name", "payments", + "--modal-function-name", "worker", + "--modal-environment", "main", + "--modal-version", "-1", + ) + s.Error(res.Err) + s.ErrorContains(res.Err, "Modal provider detail \"version\" must be a non-negative integer string") + // AWS Lambda and GCP Cloud Run providers are mutually exclusive on create. mixedProvidersBuildID := uuid.NewString() @@ -1458,7 +1503,7 @@ func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_Errors() { "--gcp-cloud-run-worker-pool", "my-worker-pool", ) s.Error(res.Err) - s.ErrorContains(res.Err, "cannot combine --aws-lambda-*, --aws-agentcore-*, and --gcp-cloud-run-* flags") + s.ErrorContains(res.Err, "cannot combine --aws-lambda-*, --aws-agentcore-*, --gcp-cloud-run-*, and --modal-* flags") // Attempting to update the compute config for a non-existent WDV // should fail. @@ -1498,7 +1543,7 @@ func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_Errors() { "--gcp-cloud-run-worker-pool", "my-worker-pool", ) s.Error(res.Err) - s.ErrorContains(res.Err, "cannot combine --aws-lambda-*, --aws-agentcore-*, and --gcp-cloud-run-* flags") + s.ErrorContains(res.Err, "cannot combine --aws-lambda-*, --aws-agentcore-*, --gcp-cloud-run-*, and --modal-* flags") // --remove cannot be combined with GCP Cloud Run flags. res = s.Execute( @@ -1512,6 +1557,18 @@ func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_Errors() { s.Error(res.Err) s.ErrorContains(res.Err, "--remove cannot be combined with") + // --remove cannot be combined with Modal provider flags either. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, + "--build-id", lazyCreatedBuildID, + "--modal-app-name", "payments", + "--remove", + ) + 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( @@ -1758,6 +1815,56 @@ func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_Errors() { s.ErrorContains(res.Err, "only valid with --gcp-cloud-run-worker-pool") } +func (s *SharedServerSuite) TestCreateWorkerDeploymentVersion_ModalRequest() { + var mu sync.Mutex + var captured *workflowservice.CreateWorkerDeploymentVersionRequest + 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.CreateWorkerDeploymentVersionRequest); 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.CreateWorkerDeploymentVersionRequest { + mu.Lock() + defer mu.Unlock() + r := captured + captured = nil + return r + } + + res := s.Execute( + "worker", "deployment", "create-version", + "--address", s.Address(), + "--deployment-name", uuid.NewString(), "--build-id", "1.0", + "--modal-app-name", "payments", + "--modal-function-name", "worker", + "--modal-environment", "main", + ) + s.NoError(res.Err) + createReq := takeCaptured() + s.NotNil(createReq) + createGroup := createReq.GetComputeConfig().GetScalingGroups()["default"] + s.Equal("modal", createGroup.GetProvider().GetType()) + s.Equal("no-sync", createGroup.GetScaler().GetType()) + s.Nil(createGroup.GetScaler().GetDetails()) + var modalDetails map[string]any + s.NoError(converter.GetDefaultDataConverter().FromPayload(createGroup.GetProvider().GetDetails(), &modalDetails)) + s.Equal("payments", modalDetails["app_name"]) + s.Equal("worker", modalDetails["function_name"]) + s.Equal("main", modalDetails["environment"]) + s.NotContains(modalDetails, "workspace_id") + s.NotContains(modalDetails, "version") +} + // 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 @@ -1837,6 +1944,32 @@ func (s *SharedServerSuite) TestUpdateWorkerDeploymentVersionComputeConfig_Updat s.Equal("aws-lambda", sg.GetScalingGroup().GetProvider().GetType()) s.Nil(sg.GetScalingGroup().GetScaler().GetDetails()) + // Switching to Modal also clears rate-based scaler details and includes the + // required environment plus optional workspace and App version. + res = s.Execute( + "worker", "deployment", "update-version-compute-config", + "--address", s.Address(), + "--deployment-name", deploymentName, "--build-id", buildID, + "--modal-app-name", "payments", + "--modal-function-name", "worker", + "--modal-environment", "staging", + "--modal-workspace-id", "ws-123", + "--modal-version", "42", + ) + s.NoError(res.Err) + req = takeCaptured() + s.NotNil(req) + sg = req.GetComputeConfigScalingGroups()["default"] + s.Contains(sg.GetUpdateMask().GetPaths(), "scaler.details") + s.Equal("modal", sg.GetScalingGroup().GetProvider().GetType()) + s.Equal("no-sync", sg.GetScalingGroup().GetScaler().GetType()) + s.Nil(sg.GetScalingGroup().GetScaler().GetDetails()) + var modalDetails map[string]any + s.NoError(converter.GetDefaultDataConverter().FromPayload(sg.GetScalingGroup().GetProvider().GetDetails(), &modalDetails)) + s.Equal("staging", modalDetails["environment"]) + s.Equal("ws-123", modalDetails["workspace_id"]) + s.Equal("42", modalDetails["version"]) + // Updating only GCP provider fields (no scaler flags) leaves scaler.details // out of the mask, so existing bounds are preserved. res = s.Execute( diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index fd74ba530..3c344cb1f 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -1267,6 +1267,19 @@ commands: --aws-agentcore-assume-role-external-id AgentcoreAssumeRoleExternalID ``` + Or pass a deployed Modal application and function that spawns a Worker in + the Worker Deployment: + + ``` + temporal worker deployment create-version \ + --namespace YourNamespaceName \ + --deployment-name YourDeploymentName \ + --build-id YourBuildID \ + --modal-app-name YourModalApp \ + --modal-function-name YourModalFunction \ + --modal-environment main + ``` + Or pass compute provider information for a GCP Cloud Run worker pool that spawns a Worker in the Worker Deployment: @@ -1352,6 +1365,33 @@ commands: When --aws-agentcore-endpoint-arn is specified, --aws-agentcore-assume-role-arn and --aws-agentcore-assume-role-external-id are required unless this flag is passed, in which case both must be omitted. + - name: modal-app-name + type: string + description: | + Name of the deployed Modal application containing the Worker + function. Required when configuring a Modal compute provider. + - name: modal-function-name + type: string + description: | + Name of the Modal function to invoke when there are no active pollers + for task queue targets in the Worker Deployment. Required when + configuring a Modal compute provider. + - name: modal-environment + type: string + description: | + Modal environment containing the deployed application. Required when + configuring a Modal compute provider. + - name: modal-workspace-id + type: string + description: | + Optional Modal workspace ID used to select the workspace's credential + grant. Omit when the server uses ambient Modal credentials. + - name: modal-version + type: string + description: | + Optional deployed Modal application version, expressed as a + non-negative integer. Omit to use the application's current deployed + version. - name: gcp-cloud-run-project type: string description: | @@ -1662,6 +1702,17 @@ commands: --aws-agentcore-assume-role-external-id UpdatedExternalID ``` + To update the Modal application and function associated with an existing + Worker Deployment Version: + + ``` + temporal worker deployment update-version-compute-config \ + --deployment-name YourDeploymentName --build-id YourBuildID \ + --modal-app-name UpdatedModalApp \ + --modal-function-name UpdatedModalFunction \ + --modal-environment main + ``` + To update the GCP Cloud Run worker pool associated with an existing Worker Deployment Version: @@ -1769,6 +1820,33 @@ commands: When --aws-agentcore-endpoint-arn is specified, --aws-agentcore-assume-role-arn and --aws-agentcore-assume-role-external-id are required unless this flag is passed, in which case both must be omitted. + - name: modal-app-name + type: string + description: | + Name of the deployed Modal application containing the Worker + function. Required when configuring a Modal compute provider. + - name: modal-function-name + type: string + description: | + Name of the Modal function to invoke when there are no active pollers + for task queue targets in the Worker Deployment. Required when + configuring a Modal compute provider. + - name: modal-environment + type: string + description: | + Modal environment containing the deployed application. Required when + configuring a Modal compute provider. + - name: modal-workspace-id + type: string + description: | + Optional Modal workspace ID used to select the workspace's credential + grant. Omit when the server uses ambient Modal credentials. + - name: modal-version + type: string + description: | + Optional deployed Modal application version, expressed as a + non-negative integer. Omit to use the application's current deployed + version. - name: gcp-cloud-run-project type: string description: |