diff --git a/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go b/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go index 522425fb5..fdd92e0cb 100644 --- a/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go +++ b/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go @@ -95,6 +95,18 @@ func (s *PostgresSetter) CreateWorkflowRun( return tx.Commit(ctx) } +// mergeWorkflowJobAgentConfig builds the JobAgentConfig that ends up on a +// workflow-triggered job. The runner row holds shared credentials (e.g. +// serverUrl, apiKey); the per-job WorkflowJobAgent.Config holds the +// per-invocation payload (e.g. template, name). Per-job values win on +// conflict, mirroring the deployment flow's runner < deployment < version +// precedence in jobeligibility. +func mergeWorkflowJobAgentConfig( + runnerConfig, perJobConfig oapi.JobAgentConfig, +) oapi.JobAgentConfig { + return oapi.DeepMergeConfigs(runnerConfig, perJobConfig) +} + func (s *PostgresSetter) dispatchJobForAgent( ctx context.Context, queries *db.Queries, @@ -107,7 +119,22 @@ func (s *PostgresSetter) dispatchJobForAgent( if err != nil { return fmt.Errorf("parse job agent id: %w", err) } - jobAgentConfig, err := json.Marshal(jobAgent.Config) + workspaceIDUUID, err := uuid.Parse(workspaceID) + if err != nil { + return fmt.Errorf("parse workspace id: %w", err) + } + runner, err := queries.GetJobAgentByID(ctx, jobAgentIDUUID) + if err != nil { + return fmt.Errorf("get job agent: %w", err) + } + if runner.WorkspaceID != workspaceIDUUID { + return fmt.Errorf( + "job agent %s does not belong to workspace %s", + jobAgentIDUUID, workspaceIDUUID, + ) + } + mergedConfig := mergeWorkflowJobAgentConfig(runner.Config, jobAgent.Config) + jobAgentConfig, err := json.Marshal(mergedConfig) if err != nil { return fmt.Errorf("marshal job agent config: %w", err) } diff --git a/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go b/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go index d1bcf32bb..23b9a8ee0 100644 --- a/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go +++ b/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go @@ -155,6 +155,54 @@ func TestResolveInputs_EmptyWorkflowInputs(t *testing.T) { assert.Equal(t, "value", resolved["extra"]) } +func TestMergeWorkflowJobAgentConfig_RunnerCredentialsPreserved(t *testing.T) { + runner := oapi.JobAgentConfig{ + "serverUrl": "https://argo.example", + "apiKey": "secret", + } + perJob := oapi.JobAgentConfig{ + "template": "apiVersion: argoproj.io/v1alpha1", + "name": "deploy", + } + + merged := mergeWorkflowJobAgentConfig(runner, perJob) + + assert.Equal(t, "https://argo.example", merged["serverUrl"]) + assert.Equal(t, "secret", merged["apiKey"]) + assert.Equal(t, "apiVersion: argoproj.io/v1alpha1", merged["template"]) + assert.Equal(t, "deploy", merged["name"]) +} + +func TestMergeWorkflowJobAgentConfig_PerJobOverridesRunner(t *testing.T) { + runner := oapi.JobAgentConfig{ + "serverUrl": "https://shared.example", + "apiKey": "secret", + } + perJob := oapi.JobAgentConfig{ + "serverUrl": "https://override.example", + "template": "spec", + } + + merged := mergeWorkflowJobAgentConfig(runner, perJob) + + assert.Equal(t, "https://override.example", merged["serverUrl"]) + assert.Equal(t, "secret", merged["apiKey"]) + assert.Equal(t, "spec", merged["template"]) +} + +func TestMergeWorkflowJobAgentConfig_NilInputs(t *testing.T) { + merged := mergeWorkflowJobAgentConfig(nil, nil) + assert.Empty(t, merged) + + runner := oapi.JobAgentConfig{"serverUrl": "https://argo.example"} + merged = mergeWorkflowJobAgentConfig(runner, nil) + assert.Equal(t, "https://argo.example", merged["serverUrl"]) + + perJob := oapi.JobAgentConfig{"template": "spec"} + merged = mergeWorkflowJobAgentConfig(nil, perJob) + assert.Equal(t, "spec", merged["template"]) +} + func TestResolveInputs_ExtraProvidedInputsPassThrough(t *testing.T) { workflow := &oapi.Workflow{ Inputs: []oapi.WorkflowInput{