Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ require (
github.com/stretchr/testify v1.12.1
github.com/temporalio/cli/cliext v0.0.0
github.com/temporalio/ui-server/v2 v2.54.1
go.temporal.io/api v1.63.5
go.temporal.io/sdk v1.47.0
go.temporal.io/api v1.64.0
go.temporal.io/sdk v1.49.0
go.temporal.io/sdk/contrib/envconfig v1.0.2
go.temporal.io/server v1.32.0
golang.org/x/exp v0.0.0-20260611194520-c48552f49976
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -481,12 +481,12 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.temporal.io/api v1.63.5 h1:c11+kPYHkXXL3UiShPdbMD+xtvqGsbTibUA9ypmiCa4=
go.temporal.io/api v1.63.5/go.mod h1:SrlW2JMwVlDP4nRWSNznUFqnSHd+YeMDS1BkYo63HCQ=
go.temporal.io/api v1.64.0 h1:Wu7TMdVN/4KdU+v0As6SMm86uapnM1yI7PhC5Hvhqso=
go.temporal.io/api v1.64.0/go.mod h1:acM0I9WPuYg8W3Pd9jOZvEgi7mRUttUQ4+e7fowKVnM=
go.temporal.io/auto-scaled-workers v0.2.0-1.32.0.158.0 h1:l+Rj0cIHMC2VB/DC+axrLLnZ2ISTgUpUw8TMiP4B9F8=
go.temporal.io/auto-scaled-workers v0.2.0-1.32.0.158.0/go.mod h1:ZGNY7kCU0EZpXx02D81/jGXHbUcFcVIzzguJ/f8pvUQ=
go.temporal.io/sdk v1.47.0 h1:lZ39w1+uWSjHTL0F3mSc0t4XUnKX8CCcWxqSiLbaHnc=
go.temporal.io/sdk v1.47.0/go.mod h1:ilKs0twgP4JpP8pfhIgZumnOEyBiYn6ZO/ta//NnKMU=
go.temporal.io/sdk v1.49.0 h1:CtGI0BUe/SCo3eoqTwuWWtXKueii9GBVus7KrKKH1Vo=
go.temporal.io/sdk v1.49.0/go.mod h1:xP0FulN5JJSfisESUP60LlWsrKz2tLSStGjVdk8r5cc=
go.temporal.io/sdk/contrib/envconfig v1.0.2 h1:MGHfsuPUtsf7X9M6WYn3zYJj/mWsuYHnA1uuiL0KEuE=
go.temporal.io/sdk/contrib/envconfig v1.0.2/go.mod h1:MuMiH7hksps2uXnmKuAWaP9P6WbkSDy62kl64t1VJVg=
go.temporal.io/server v1.32.0 h1:JQoqsVREaGc8vcuDNfuRlySiV3tVi81XUOVC9LeL/1Y=
Expand Down
2 changes: 1 addition & 1 deletion internal/temporalcli/commands.activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func buildStartActivityOptions(opts *ActivityStartOptions) (client.StartActivity
HeartbeatTimeout: opts.HeartbeatTimeout.Duration(),
StartDelay: opts.StartDelay.Duration(),
Summary: opts.StaticSummary,
Details: opts.StaticDetails,
StaticDetails: opts.StaticDetails,
Priority: temporal.Priority{
PriorityKey: opts.PriorityKey,
FairnessKey: opts.FairnessKey,
Expand Down
149 changes: 139 additions & 10 deletions internal/temporalcli/commands.gen.go

Large diffs are not rendered by default.

53 changes: 44 additions & 9 deletions internal/temporalcli/commands.worker.deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,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 {
Expand All @@ -1205,24 +1210,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 {
Expand Down Expand Up @@ -1250,19 +1265,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,
Expand Down Expand Up @@ -1396,6 +1421,11 @@ func (c *TemporalWorkerDeploymentCreateVersionCommand) run(cctx *CommandContext,
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
Expand Down Expand Up @@ -1493,12 +1523,17 @@ func (c *TemporalWorkerDeploymentUpdateVersionComputeConfigCommand) run(cctx *Co
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/stretchr/testify/require"
"github.com/temporalio/cli/internal/printer"
commonpb "go.temporal.io/api/common/v1"
computepb "go.temporal.io/api/compute/v1"
deploymentpb "go.temporal.io/api/deployment/v1"
enumspb "go.temporal.io/api/enums/v1"
Expand All @@ -24,6 +25,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},
Expand All @@ -45,12 +47,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,21 +166,55 @@ func (s *SharedServerSuite) TestComputeConfig_AwsLambda_UpdateError() {
s.ErrorContains(res.Err, "error updating worker deployment version compute config: invalid compute config")
}

func (s *SharedServerSuite) TestComputeConfig_AwsLambda_TaskQueueTypes() {
func (s *SharedServerSuite) TestComputeConfig_AwsLambda_TaskQueueTypesAndRegions() {
fake := s.newFakeComputeConfigServer()
fake.setComputeConfig(nil)

res := s.Execute(lambdaComputeConfigArgs("update", s.Address(), uuid.NewString(),
"--scaling-group", "use1",
"--task-queue-type", "nexus",
"--task-queue-type", "workflow",
"--region-id", "aws-us-east-1",
)...)
s.NoError(res.Err)
s.Empty(res.Stderr.String())
updates := fake.takeUpdates()
s.Len(updates, 1)
sg := updates[0].GetComputeConfigScalingGroups()["use1"].GetScalingGroup()
s.Equal([]enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW, enumspb.TASK_QUEUE_TYPE_NEXUS}, sg.GetTaskQueueTypes())
s.Equal([]string{"aws-us-east-1"}, sg.GetRegionIds())
}

func (s *SharedServerSuite) TestComputeConfig_AwsLambda_RegionIDs() {
fake := s.newFakeComputeConfigServer()
fake.setComputeConfig(nil)
deploymentName := uuid.NewString()
regionArgs := lambdaComputeConfigArgs("update", s.Address(), deploymentName,
"--scaling-group", "aws-us-west-2",
"--region-id", "aws-us-west-2",
"--region-id", "aws-us-east-1",
"--region-id", "aws-us-west-2",
)

// Region IDs are de-duplicated and sorted, and the mask covers them.
res := s.Execute(regionArgs...)
s.NoError(res.Err)
s.Empty(res.Stderr.String())
updates := fake.takeUpdates()
s.Len(updates, 1)
update := updates[0].GetComputeConfigScalingGroups()["aws-us-west-2"]
s.Equal([]string{"aws-us-east-1", "aws-us-west-2"}, update.GetScalingGroup().GetRegionIds())
s.Equal([]string{"region_ids", "provider.type", "provider.details", "scaler.type", "scaler.details"}, update.GetUpdateMask().GetPaths())

// Leaving out --region-id keeps the region IDs.
res = s.Execute(lambdaComputeConfigArgs("update", s.Address(), deploymentName, "--scaling-group", "aws-us-west-2")...)
s.NoError(res.Err)
updates = fake.takeUpdates()
s.Len(updates, 1)
update = updates[0].GetComputeConfigScalingGroups()["aws-us-west-2"]
s.Empty(update.GetScalingGroup().GetRegionIds())
s.NotContains(update.GetUpdateMask().GetPaths(), "region_ids")
s.Equal([]string{"aws-us-east-1", "aws-us-west-2"}, fake.computeConfig.GetScalingGroups()["aws-us-west-2"].GetRegionIds())
}

func (s *SharedServerSuite) TestComputeConfig_AwsLambda_ScalerSettings() {
Expand Down Expand Up @@ -263,13 +297,24 @@ func (s *SharedServerSuite) TestComputeConfig_AwsLambda_PartialUpdate() {
s.Equal("no-sync", sg.GetScaler().GetType())
s.Equal(map[string]any{"scale_up_cooloff_ms": float64(500)}, s.decodePayloadMap(sg.GetScaler().GetDetails()))
s.Equal([]enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, sg.GetTaskQueueTypes())
s.Equal([]string{"aws-us-west-2"}, sg.GetRegionIds())

// --region-id on its own changes only the region IDs.
res = s.Execute(args("--scaling-group", "aws-us-east-1-wf", "--region-id", "aws-us-west-2", "--region-id", "aws-us-east-1")...)
s.NoError(res.Err)
s.Equal([]string{"region_ids"}, onlyUpdate("aws-us-east-1-wf").GetUpdateMask().GetPaths())
sg = stored("aws-us-east-1-wf")
s.Equal([]string{"aws-us-east-1", "aws-us-west-2"}, sg.GetRegionIds())
s.Equal([]enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_WORKFLOW}, sg.GetTaskQueueTypes())
s.Equal("aws-lambda", sg.GetProvider().GetType())

// --task-queue-type on its own changes only the Task Queue types.
res = s.Execute(args("--scaling-group", "aws-us-east-1-wf", "--task-queue-type", "nexus")...)
s.NoError(res.Err)
s.Equal([]string{"task_queue_types"}, onlyUpdate("aws-us-east-1-wf").GetUpdateMask().GetPaths())
sg = stored("aws-us-east-1-wf")
s.Equal([]enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_NEXUS}, sg.GetTaskQueueTypes())
s.Equal([]string{"aws-us-east-1", "aws-us-west-2"}, sg.GetRegionIds())
s.Equal("aws-lambda", sg.GetProvider().GetType())
s.Equal("no-sync", sg.GetScaler().GetType())

Expand All @@ -285,6 +330,7 @@ func (s *SharedServerSuite) TestComputeConfig_AwsLambda_PartialUpdate() {
s.Equal("no-sync", sg.GetScaler().GetType())
s.Nil(sg.GetScaler().GetDetails())
s.Equal([]enumspb.TaskQueueType{enumspb.TASK_QUEUE_TYPE_ACTIVITY}, sg.GetTaskQueueTypes())
s.Equal([]string{"aws-us-west-2"}, sg.GetRegionIds())

// The provider flags are passed together, and something must be passed.
for _, tc := range []struct {
Expand Down Expand Up @@ -365,6 +411,12 @@ func (s *SharedServerSuite) TestComputeConfig_Provider_Errors() {
"--function-arn", "arn", "--skip-role-and-external-id", "--scaling-group", ""),
errText: "--scaling-group cannot be empty",
},
{
name: "empty region ID",
args: append(append([]string{}, lambda...),
"--function-arn", "arn", "--skip-role-and-external-id", "--region-id", ""),
errText: "--region-id cannot be empty",
},
} {
res := s.Execute(tc.args...)
s.ErrorContains(res.Err, tc.errText, tc.name)
Expand Down
Loading
Loading