diff --git a/apps/workspace-engine/pkg/workspace/jobagents/argo/argoapp.go b/apps/workspace-engine/pkg/workspace/jobagents/argo/argoapp.go index 0827e1e00..cee2a177a 100644 --- a/apps/workspace-engine/pkg/workspace/jobagents/argo/argoapp.go +++ b/apps/workspace-engine/pkg/workspace/jobagents/argo/argoapp.go @@ -48,13 +48,6 @@ func (a *ArgoApplication) Type() string { return "argo-cd" } -func (a *ArgoApplication) Supports() types.Capabilities { - return types.Capabilities{ - Workflows: true, - Deployments: true, - } -} - func (a *ArgoApplication) Dispatch(ctx context.Context, dispatchCtx types.DispatchContext) error { jobAgentConfig := dispatchCtx.JobAgentConfig serverAddr, apiKey, template, err := a.parseJobAgentConfig(jobAgentConfig) diff --git a/apps/workspace-engine/pkg/workspace/jobagents/github/githubaction.go b/apps/workspace-engine/pkg/workspace/jobagents/github/githubaction.go index 557fd7e42..05a6f7607 100644 --- a/apps/workspace-engine/pkg/workspace/jobagents/github/githubaction.go +++ b/apps/workspace-engine/pkg/workspace/jobagents/github/githubaction.go @@ -36,13 +36,6 @@ func (a *GithubAction) Type() string { return "github-app" } -func (a *GithubAction) Supports() types.Capabilities { - return types.Capabilities{ - Workflows: true, - Deployments: true, - } -} - // Dispatch implements types.Dispatchable. func (a *GithubAction) Dispatch(ctx context.Context, dispatchCtx types.DispatchContext) error { cfg, err := a.parseJobAgentConfig(dispatchCtx.JobAgentConfig) diff --git a/apps/workspace-engine/pkg/workspace/jobagents/registry.go b/apps/workspace-engine/pkg/workspace/jobagents/registry.go index 20cc2b9b2..7e0ad7ed3 100644 --- a/apps/workspace-engine/pkg/workspace/jobagents/registry.go +++ b/apps/workspace-engine/pkg/workspace/jobagents/registry.go @@ -37,6 +37,72 @@ func (r *Registry) Register(dispatcher types.Dispatchable) { r.dispatchers[dispatcher.Type()] = dispatcher } +func (r *Registry) fillReleaseContext(job *oapi.Job, ctx *types.DispatchContext) error { + releaseId := job.ReleaseId + if releaseId == "" { + return nil + } + + jobWithRelease, err := r.store.Jobs.GetWithRelease(job.Id) + if err != nil { + return fmt.Errorf("failed to get job with release: %w", err) + } + + ctx.Release = &jobWithRelease.Release + ctx.Deployment = jobWithRelease.Deployment + ctx.Environment = jobWithRelease.Environment + ctx.Resource = jobWithRelease.Resource + ctx.Version = &jobWithRelease.Release.Version + + return nil +} + +func (r *Registry) fillWorkflowContext(job *oapi.Job, ctx *types.DispatchContext) error { + if job.WorkflowJobId == "" { + return nil + } + + workflowJob, ok := r.store.WorkflowJobs.Get(job.WorkflowJobId) + if !ok { + return fmt.Errorf("workflow job not found: %s", job.WorkflowJobId) + } + + workflowRun, ok := r.store.WorkflowRuns.Get(workflowJob.WorkflowRunId) + if !ok { + return fmt.Errorf("workflow run not found: %s", workflowJob.WorkflowRunId) + } + + ctx.WorkflowJob = workflowJob + ctx.WorkflowRun = workflowRun + return nil +} + +func (r *Registry) getMergedJobAgentConfig(jobAgent *oapi.JobAgent, ctx *types.DispatchContext) (oapi.JobAgentConfig, error) { + agentConfig := jobAgent.Config + + var workflowJobConfig oapi.JobAgentConfig + if ctx.WorkflowJob != nil { + workflowJobConfig = ctx.WorkflowJob.Config + } + + var deploymentConfig oapi.JobAgentConfig + if ctx.Deployment != nil { + deploymentConfig = ctx.Deployment.JobAgentConfig + } + + var versionConfig oapi.JobAgentConfig + if ctx.Version != nil { + versionConfig = ctx.Version.JobAgentConfig + } + + mergedConfig, err := mergeJobAgentConfig(agentConfig, deploymentConfig, workflowJobConfig, versionConfig) + if err != nil { + return oapi.JobAgentConfig{}, fmt.Errorf("failed to merge job agent configs: %w", err) + } + + return mergedConfig, nil +} + func (r *Registry) Dispatch(ctx context.Context, job *oapi.Job) error { jobAgent, ok := r.store.JobAgents.Get(job.JobAgentId) if !ok { @@ -52,42 +118,20 @@ func (r *Registry) Dispatch(ctx context.Context, job *oapi.Job) error { dispatchContext.Job = job dispatchContext.JobAgent = jobAgent - isWorkflow := job.WorkflowJobId != "" - caps := dispatcher.Supports() - - if isWorkflow && !caps.Workflows { - return fmt.Errorf("job agent type %s does not support workflows", jobAgent.Type) + if err := r.fillReleaseContext(job, &dispatchContext); err != nil { + return fmt.Errorf("failed to get release context: %w", err) } - - if !isWorkflow && !caps.Deployments { - return fmt.Errorf("job agent type %s does not support deployments", jobAgent.Type) + if err := r.fillWorkflowContext(job, &dispatchContext); err != nil { + return fmt.Errorf("failed to get workflow context: %w", err) } - - if jobWithRelease, _ := r.store.Jobs.GetWithRelease(job.Id); jobWithRelease != nil { - dispatchContext.Release = &jobWithRelease.Release - dispatchContext.Deployment = jobWithRelease.Deployment - dispatchContext.Environment = jobWithRelease.Environment - dispatchContext.Resource = jobWithRelease.Resource - jobAgentConfig, err := mergeJobAgentConfig( - jobAgent.Config, - jobWithRelease.Deployment.JobAgentConfig, - jobWithRelease.Release.Version.JobAgentConfig, - ) - if err != nil { - return fmt.Errorf("failed to merge job agent config: %w", err) - } - dispatchContext.JobAgentConfig = jobAgentConfig - } - - if workflowJob, ok := r.store.WorkflowJobs.Get(job.WorkflowJobId); ok { - dispatchContext.WorkflowJob = workflowJob - if workflowRun, ok := r.store.WorkflowRuns.Get(workflowJob.WorkflowRunId); ok { - dispatchContext.WorkflowRun = workflowRun - } - dispatchContext.JobAgent = jobAgent - dispatchContext.JobAgentConfig = job.JobAgentConfig + mergedConfig, err := r.getMergedJobAgentConfig(jobAgent, &dispatchContext) + if err != nil { + return fmt.Errorf("failed to merge all job agent configs: %w", err) } + dispatchContext.JobAgentConfig = mergedConfig + job.JobAgentConfig = mergedConfig + r.store.Jobs.Upsert(ctx, job) return dispatcher.Dispatch(ctx, dispatchContext) } diff --git a/apps/workspace-engine/pkg/workspace/jobagents/registry_test.go b/apps/workspace-engine/pkg/workspace/jobagents/registry_test.go new file mode 100644 index 000000000..f138b8c0d --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/jobagents/registry_test.go @@ -0,0 +1,761 @@ +package jobagents + +import ( + "context" + "testing" + "time" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/statechange" + "workspace-engine/pkg/workspace/jobagents/types" + "workspace-engine/pkg/workspace/store" + + "github.com/stretchr/testify/assert" +) + +type fakeDispatcher struct { + dispatchedContexts []types.DispatchContext +} + +func (f *fakeDispatcher) Type() string { + return "fake" +} + +func (f *fakeDispatcher) Dispatch(ctx context.Context, dc types.DispatchContext) error { + f.dispatchedContexts = append(f.dispatchedContexts, dc) + return nil +} + +func newTestRegistry(s *store.Store) (*Registry, *fakeDispatcher) { + fake := &fakeDispatcher{} + r := &Registry{ + dispatchers: make(map[string]types.Dispatchable), + store: s, + } + r.Register(fake) + return r, fake +} + +func newTestStore() *store.Store { + return store.New("test-workspace", statechange.NewChangeSet[any]()) +} + +func TestDeepMerge_BasicMerge(t *testing.T) { + dst := map[string]any{"a": "1"} + src := map[string]any{"b": "2"} + deepMerge(dst, src) + + assert.Equal(t, "1", dst["a"]) + assert.Equal(t, "2", dst["b"]) +} + +func TestDeepMerge_OverridesScalarValues(t *testing.T) { + dst := map[string]any{"a": "old"} + src := map[string]any{"a": "new"} + deepMerge(dst, src) + + assert.Equal(t, "new", dst["a"]) +} + +func TestDeepMerge_NestedMapsAreMergedRecursively(t *testing.T) { + dst := map[string]any{ + "nested": map[string]any{ + "keep": "yes", + "old": "value", + }, + } + src := map[string]any{ + "nested": map[string]any{ + "old": "updated", + "new": "added", + }, + } + deepMerge(dst, src) + + nested := dst["nested"].(map[string]any) + assert.Equal(t, "yes", nested["keep"]) + assert.Equal(t, "updated", nested["old"]) + assert.Equal(t, "added", nested["new"]) +} + +func TestDeepMerge_ScalarOverridesNestedMap(t *testing.T) { + dst := map[string]any{ + "key": map[string]any{"a": "1"}, + } + src := map[string]any{ + "key": "scalar", + } + deepMerge(dst, src) + + assert.Equal(t, "scalar", dst["key"]) +} + +func TestDeepMerge_NestedMapOverridesScalar(t *testing.T) { + dst := map[string]any{ + "key": "scalar", + } + src := map[string]any{ + "key": map[string]any{"a": "1"}, + } + deepMerge(dst, src) + + assert.Equal(t, map[string]any{"a": "1"}, dst["key"]) +} + +func TestDeepMerge_NilSource(t *testing.T) { + dst := map[string]any{"a": "1"} + deepMerge(dst, nil) + + assert.Equal(t, "1", dst["a"]) + assert.Len(t, dst, 1) +} + +func TestMergeJobAgentConfig_SingleConfig(t *testing.T) { + config := oapi.JobAgentConfig{"key": "value"} + + merged, err := mergeJobAgentConfig(config) + + assert.NoError(t, err) + assert.Equal(t, "value", merged["key"]) +} + +func TestMergeJobAgentConfig_LaterConfigsOverrideEarlier(t *testing.T) { + base := oapi.JobAgentConfig{"shared": "base", "base_only": "yes"} + override := oapi.JobAgentConfig{"shared": "override", "override_only": "yes"} + + merged, err := mergeJobAgentConfig(base, override) + + assert.NoError(t, err) + assert.Equal(t, "override", merged["shared"]) + assert.Equal(t, "yes", merged["base_only"]) + assert.Equal(t, "yes", merged["override_only"]) +} + +func TestMergeJobAgentConfig_ThreeConfigs(t *testing.T) { + first := oapi.JobAgentConfig{"a": "1", "shared": "first"} + second := oapi.JobAgentConfig{"b": "2", "shared": "second"} + third := oapi.JobAgentConfig{"c": "3", "shared": "third"} + + merged, err := mergeJobAgentConfig(first, second, third) + + assert.NoError(t, err) + assert.Equal(t, "1", merged["a"]) + assert.Equal(t, "2", merged["b"]) + assert.Equal(t, "3", merged["c"]) + assert.Equal(t, "third", merged["shared"]) +} + +func TestMergeJobAgentConfig_EmptyConfigs(t *testing.T) { + merged, err := mergeJobAgentConfig(oapi.JobAgentConfig{}, oapi.JobAgentConfig{}) + + assert.NoError(t, err) + assert.Empty(t, merged) +} + +func TestMergeJobAgentConfig_NilConfigs(t *testing.T) { + merged, err := mergeJobAgentConfig(nil, oapi.JobAgentConfig{"a": "1"}, nil) + + assert.NoError(t, err) + assert.Equal(t, "1", merged["a"]) +} + +func TestFillReleaseContext_NoReleaseId(t *testing.T) { + s := newTestStore() + r, _ := newTestRegistry(s) + + job := &oapi.Job{Id: "job-1", ReleaseId: ""} + dispatchCtx := &types.DispatchContext{} + + err := r.fillReleaseContext(job, dispatchCtx) + + assert.NoError(t, err) + assert.Nil(t, dispatchCtx.Release) + assert.Nil(t, dispatchCtx.Deployment) + assert.Nil(t, dispatchCtx.Environment) + assert.Nil(t, dispatchCtx.Resource) +} + +func TestFillReleaseContext_PopulatesAllFields(t *testing.T) { + ctx := context.Background() + s := newTestStore() + r, _ := newTestRegistry(s) + + env := &oapi.Environment{Id: "env-1", Name: "production", SystemId: "sys-1"} + dep := &oapi.Deployment{Id: "dep-1", Name: "api", SystemId: "sys-1", Metadata: map[string]string{}, JobAgentConfig: oapi.JobAgentConfig{}} + res := &oapi.Resource{ + Id: "res-1", Name: "cluster", Kind: "kubernetes", Identifier: "cluster-1", + Version: "v1", WorkspaceId: "test-workspace", + Config: map[string]interface{}{}, Metadata: map[string]string{}, + CreatedAt: time.Now(), + } + s.Environments.Upsert(ctx, env) + s.Deployments.Upsert(ctx, dep) + s.Resources.Upsert(ctx, res) + + release := &oapi.Release{ + ReleaseTarget: oapi.ReleaseTarget{ + EnvironmentId: "env-1", + DeploymentId: "dep-1", + ResourceId: "res-1", + }, + Version: oapi.DeploymentVersion{Id: "ver-1", Tag: "v1.0.0"}, + Variables: map[string]oapi.LiteralValue{}, + } + _ = s.Releases.Upsert(ctx, release) + + job := &oapi.Job{ + Id: "job-1", + ReleaseId: release.ID(), + Status: oapi.JobStatusPending, + Metadata: map[string]string{}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + s.Jobs.Upsert(ctx, job) + + dispatchCtx := &types.DispatchContext{} + err := r.fillReleaseContext(job, dispatchCtx) + + assert.NoError(t, err) + assert.NotNil(t, dispatchCtx.Release) + assert.Equal(t, "env-1", dispatchCtx.Release.ReleaseTarget.EnvironmentId) + assert.NotNil(t, dispatchCtx.Deployment) + assert.Equal(t, "dep-1", dispatchCtx.Deployment.Id) + assert.NotNil(t, dispatchCtx.Environment) + assert.Equal(t, "env-1", dispatchCtx.Environment.Id) + assert.NotNil(t, dispatchCtx.Resource) + assert.Equal(t, "res-1", dispatchCtx.Resource.Id) +} + +func TestFillWorkflowContext_NoWorkflowJobId(t *testing.T) { + s := newTestStore() + r, _ := newTestRegistry(s) + + job := &oapi.Job{Id: "job-1", WorkflowJobId: ""} + dispatchCtx := &types.DispatchContext{} + + err := r.fillWorkflowContext(job, dispatchCtx) + + assert.NoError(t, err) + assert.Nil(t, dispatchCtx.WorkflowJob) + assert.Nil(t, dispatchCtx.WorkflowRun) +} + +func TestFillWorkflowContext_PopulatesWorkflowRunAndJob(t *testing.T) { + ctx := context.Background() + s := newTestStore() + r, _ := newTestRegistry(s) + + workflowRun := &oapi.WorkflowRun{ + Id: "wf-run-1", + WorkflowId: "wf-1", + Inputs: map[string]interface{}{"version": "1.0"}, + } + s.WorkflowRuns.Upsert(ctx, workflowRun) + + workflowJob := &oapi.WorkflowJob{ + Id: "wf-job-1", + WorkflowRunId: "wf-run-1", + Ref: "fake", + Index: 0, + Config: map[string]interface{}{}, + } + s.WorkflowJobs.Upsert(ctx, workflowJob) + + job := &oapi.Job{Id: "job-1", WorkflowJobId: "wf-job-1"} + dispatchCtx := &types.DispatchContext{} + + err := r.fillWorkflowContext(job, dispatchCtx) + + assert.NoError(t, err) + assert.NotNil(t, dispatchCtx.WorkflowJob) + assert.Equal(t, "wf-job-1", dispatchCtx.WorkflowJob.Id) + assert.NotNil(t, dispatchCtx.WorkflowRun) + assert.Equal(t, "wf-run-1", dispatchCtx.WorkflowRun.Id) + assert.Equal(t, map[string]interface{}{"version": "1.0"}, dispatchCtx.WorkflowRun.Inputs) +} + +func TestFillWorkflowContext_WorkflowJobNotFound(t *testing.T) { + s := newTestStore() + r, _ := newTestRegistry(s) + + job := &oapi.Job{Id: "job-1", WorkflowJobId: "nonexistent"} + dispatchCtx := &types.DispatchContext{} + + err := r.fillWorkflowContext(job, dispatchCtx) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "workflow job not found") +} + +func TestFillWorkflowContext_WorkflowRunNotFound(t *testing.T) { + ctx := context.Background() + s := newTestStore() + r, _ := newTestRegistry(s) + + workflowJob := &oapi.WorkflowJob{ + Id: "wf-job-1", + WorkflowRunId: "nonexistent-run", + Ref: "fake", + Index: 0, + Config: map[string]interface{}{}, + } + s.WorkflowJobs.Upsert(ctx, workflowJob) + + job := &oapi.Job{Id: "job-1", WorkflowJobId: "wf-job-1"} + dispatchCtx := &types.DispatchContext{} + + err := r.fillWorkflowContext(job, dispatchCtx) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "workflow run not found") +} + +func TestGetMergedJobAgentConfig_AgentConfigOnly(t *testing.T) { + s := newTestStore() + r, _ := newTestRegistry(s) + + agent := &oapi.JobAgent{ + Config: oapi.JobAgentConfig{"agent_key": "agent_value"}, + } + dispatchCtx := &types.DispatchContext{} + + merged, err := r.getMergedJobAgentConfig(agent, dispatchCtx) + + assert.NoError(t, err) + assert.Equal(t, "agent_value", merged["agent_key"]) +} + +func TestGetMergedJobAgentConfig_WithWorkflowJobConfig(t *testing.T) { + s := newTestStore() + r, _ := newTestRegistry(s) + + agent := &oapi.JobAgent{ + Config: oapi.JobAgentConfig{"shared": "agent", "agent_only": "yes"}, + } + dispatchCtx := &types.DispatchContext{ + WorkflowJob: &oapi.WorkflowJob{ + Config: map[string]interface{}{"shared": "workflow", "wf_only": "yes"}, + }, + } + + merged, err := r.getMergedJobAgentConfig(agent, dispatchCtx) + + assert.NoError(t, err) + assert.Equal(t, "workflow", merged["shared"]) + assert.Equal(t, "yes", merged["agent_only"]) + assert.Equal(t, "yes", merged["wf_only"]) +} + +func TestGetMergedJobAgentConfig_WithDeploymentConfig(t *testing.T) { + s := newTestStore() + r, _ := newTestRegistry(s) + + agent := &oapi.JobAgent{ + Config: oapi.JobAgentConfig{"shared": "agent"}, + } + dispatchCtx := &types.DispatchContext{ + Deployment: &oapi.Deployment{ + JobAgentConfig: oapi.JobAgentConfig{"shared": "deployment", "deploy_only": "yes"}, + }, + } + + merged, err := r.getMergedJobAgentConfig(agent, dispatchCtx) + + assert.NoError(t, err) + assert.Equal(t, "deployment", merged["shared"]) + assert.Equal(t, "yes", merged["deploy_only"]) +} + +func TestGetMergedJobAgentConfig_AllSourcesMergeInOrder(t *testing.T) { + // Priority (lowest → highest): agent → deployment → workflow → version + s := newTestStore() + r, _ := newTestRegistry(s) + + agent := &oapi.JobAgent{ + Config: oapi.JobAgentConfig{"shared": "agent", "agent_only": "a"}, + } + dispatchCtx := &types.DispatchContext{ + WorkflowJob: &oapi.WorkflowJob{ + Config: map[string]interface{}{"shared": "workflow", "wf_only": "w", "wf_deploy_shared": "workflow"}, + }, + Deployment: &oapi.Deployment{ + JobAgentConfig: oapi.JobAgentConfig{"shared": "deployment", "deploy_only": "d", "wf_deploy_shared": "deployment"}, + }, + Version: &oapi.DeploymentVersion{ + JobAgentConfig: oapi.JobAgentConfig{"shared": "version", "version_only": "v"}, + }, + } + + merged, err := r.getMergedJobAgentConfig(agent, dispatchCtx) + + assert.NoError(t, err) + assert.Equal(t, "version", merged["shared"]) + assert.Equal(t, "a", merged["agent_only"]) + assert.Equal(t, "w", merged["wf_only"]) + assert.Equal(t, "d", merged["deploy_only"]) + assert.Equal(t, "v", merged["version_only"]) + assert.Equal(t, "workflow", merged["wf_deploy_shared"], + "workflow job config should override deployment config") +} + +func TestGetMergedJobAgentConfig_NilWorkflowJobAndDeployment(t *testing.T) { + s := newTestStore() + r, _ := newTestRegistry(s) + + agent := &oapi.JobAgent{ + Config: oapi.JobAgentConfig{"key": "value"}, + } + dispatchCtx := &types.DispatchContext{} + + merged, err := r.getMergedJobAgentConfig(agent, dispatchCtx) + + assert.NoError(t, err) + assert.Equal(t, "value", merged["key"]) + assert.Len(t, merged, 1) +} + +func TestGetMergedJobAgentConfig_DeepMergeNestedConfigs(t *testing.T) { + s := newTestStore() + r, _ := newTestRegistry(s) + + agent := &oapi.JobAgent{ + Config: oapi.JobAgentConfig{ + "nested": map[string]any{"agent_key": "agent_val", "shared_key": "agent"}, + }, + } + dispatchCtx := &types.DispatchContext{ + WorkflowJob: &oapi.WorkflowJob{ + Config: map[string]interface{}{ + "nested": map[string]any{"wf_key": "wf_val", "shared_key": "workflow"}, + }, + }, + } + + merged, err := r.getMergedJobAgentConfig(agent, dispatchCtx) + + assert.NoError(t, err) + nested := merged["nested"].(map[string]any) + assert.Equal(t, "agent_val", nested["agent_key"]) + assert.Equal(t, "wf_val", nested["wf_key"]) + assert.Equal(t, "workflow", nested["shared_key"]) +} + +func TestDispatch_JobAgentNotFound(t *testing.T) { + s := newTestStore() + r, _ := newTestRegistry(s) + + job := &oapi.Job{Id: "job-1", JobAgentId: "nonexistent"} + + err := r.Dispatch(context.Background(), job) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "job agent") + assert.Contains(t, err.Error(), "not found") +} + +func TestDispatch_DispatcherTypeNotFound(t *testing.T) { + ctx := context.Background() + s := newTestStore() + r, _ := newTestRegistry(s) + + agent := &oapi.JobAgent{Id: "agent-1", Name: "agent-1", Type: "unknown-type", Config: oapi.JobAgentConfig{}} + s.JobAgents.Upsert(ctx, agent) + + job := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", + Status: oapi.JobStatusPending, Metadata: map[string]string{}, + CreatedAt: time.Now(), UpdatedAt: time.Now(), + } + + err := r.Dispatch(ctx, job) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "job agent type") + assert.Contains(t, err.Error(), "not found") +} + +func TestDispatch_WorkflowJobContextPassedToDispatcher(t *testing.T) { + ctx := context.Background() + s := newTestStore() + r, fake := newTestRegistry(s) + + agent := &oapi.JobAgent{Id: "agent-1", Name: "agent-1", Type: "fake", Config: oapi.JobAgentConfig{"base": "config"}} + s.JobAgents.Upsert(ctx, agent) + + workflowRun := &oapi.WorkflowRun{Id: "wf-run-1", WorkflowId: "wf-1", Inputs: map[string]interface{}{"env": "prod"}} + s.WorkflowRuns.Upsert(ctx, workflowRun) + + workflowJob := &oapi.WorkflowJob{ + Id: "wf-job-1", WorkflowRunId: "wf-run-1", Ref: "agent-1", Index: 0, + Config: map[string]interface{}{"job_key": "job_val"}, + } + s.WorkflowJobs.Upsert(ctx, workflowJob) + + job := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", WorkflowJobId: "wf-job-1", + Status: oapi.JobStatusPending, Metadata: map[string]string{}, + CreatedAt: time.Now(), UpdatedAt: time.Now(), + } + + err := r.Dispatch(ctx, job) + + assert.NoError(t, err) + assert.Len(t, fake.dispatchedContexts, 1) + + dc := fake.dispatchedContexts[0] + assert.Equal(t, "job-1", dc.Job.Id) + assert.Equal(t, "agent-1", dc.JobAgent.Id) + assert.NotNil(t, dc.WorkflowJob) + assert.Equal(t, "wf-job-1", dc.WorkflowJob.Id) + assert.NotNil(t, dc.WorkflowRun) + assert.Equal(t, "wf-run-1", dc.WorkflowRun.Id) +} + +func TestDispatch_MergedConfigPassedToDispatcher(t *testing.T) { + ctx := context.Background() + s := newTestStore() + r, fake := newTestRegistry(s) + + agent := &oapi.JobAgent{ + Id: "agent-1", Name: "agent-1", Type: "fake", + Config: oapi.JobAgentConfig{"agent_key": "agent_val", "shared": "agent"}, + } + s.JobAgents.Upsert(ctx, agent) + + workflowRun := &oapi.WorkflowRun{Id: "wf-run-1", WorkflowId: "wf-1", Inputs: map[string]interface{}{}} + s.WorkflowRuns.Upsert(ctx, workflowRun) + + workflowJob := &oapi.WorkflowJob{ + Id: "wf-job-1", WorkflowRunId: "wf-run-1", Ref: "agent-1", Index: 0, + Config: map[string]interface{}{"wf_key": "wf_val", "shared": "workflow"}, + } + s.WorkflowJobs.Upsert(ctx, workflowJob) + + job := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", WorkflowJobId: "wf-job-1", + Status: oapi.JobStatusPending, Metadata: map[string]string{}, + CreatedAt: time.Now(), UpdatedAt: time.Now(), + } + + err := r.Dispatch(ctx, job) + + assert.NoError(t, err) + dc := fake.dispatchedContexts[0] + assert.Equal(t, "agent_val", dc.JobAgentConfig["agent_key"]) + assert.Equal(t, "wf_val", dc.JobAgentConfig["wf_key"]) + assert.Equal(t, "workflow", dc.JobAgentConfig["shared"]) +} + +func TestDispatch_NoWorkflowNoRelease(t *testing.T) { + ctx := context.Background() + s := newTestStore() + r, fake := newTestRegistry(s) + + agent := &oapi.JobAgent{ + Id: "agent-1", Name: "agent-1", Type: "fake", + Config: oapi.JobAgentConfig{"key": "value"}, + } + s.JobAgents.Upsert(ctx, agent) + + job := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", + Status: oapi.JobStatusPending, Metadata: map[string]string{}, + CreatedAt: time.Now(), UpdatedAt: time.Now(), + } + + err := r.Dispatch(ctx, job) + + assert.NoError(t, err) + assert.Len(t, fake.dispatchedContexts, 1) + + dc := fake.dispatchedContexts[0] + assert.Nil(t, dc.Release) + assert.Nil(t, dc.Deployment) + assert.Nil(t, dc.Environment) + assert.Nil(t, dc.Resource) + assert.Nil(t, dc.WorkflowJob) + assert.Nil(t, dc.WorkflowRun) + assert.Equal(t, "value", dc.JobAgentConfig["key"]) +} + +func TestDispatch_UpsertJobInStore(t *testing.T) { + ctx := context.Background() + s := newTestStore() + r, _ := newTestRegistry(s) + + agent := &oapi.JobAgent{ + Id: "agent-1", Name: "agent-1", Type: "fake", + Config: oapi.JobAgentConfig{}, + } + s.JobAgents.Upsert(ctx, agent) + + job := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", + Status: oapi.JobStatusPending, Metadata: map[string]string{}, + CreatedAt: time.Now(), UpdatedAt: time.Now(), + } + + err := r.Dispatch(ctx, job) + + assert.NoError(t, err) + stored, ok := s.Jobs.Get("job-1") + assert.True(t, ok) + assert.Equal(t, "job-1", stored.Id) +} + +func TestDispatch_ReleaseContextPopulated(t *testing.T) { + ctx := context.Background() + s := newTestStore() + r, fake := newTestRegistry(s) + + agent := &oapi.JobAgent{ + Id: "agent-1", Name: "agent-1", Type: "fake", + Config: oapi.JobAgentConfig{}, + } + s.JobAgents.Upsert(ctx, agent) + + env := &oapi.Environment{Id: "env-1", Name: "staging", SystemId: "sys-1"} + dep := &oapi.Deployment{ + Id: "dep-1", Name: "web", SystemId: "sys-1", + Metadata: map[string]string{}, JobAgentConfig: oapi.JobAgentConfig{"deploy_cfg": "yes"}, + } + res := &oapi.Resource{ + Id: "res-1", Name: "node", Kind: "vm", Identifier: "node-1", + Version: "v1", WorkspaceId: "test-workspace", + Config: map[string]interface{}{}, Metadata: map[string]string{}, + CreatedAt: time.Now(), + } + s.Environments.Upsert(ctx, env) + s.Deployments.Upsert(ctx, dep) + s.Resources.Upsert(ctx, res) + + release := &oapi.Release{ + ReleaseTarget: oapi.ReleaseTarget{ + EnvironmentId: "env-1", + DeploymentId: "dep-1", + ResourceId: "res-1", + }, + Version: oapi.DeploymentVersion{Id: "ver-1", Tag: "v2.0.0"}, + Variables: map[string]oapi.LiteralValue{}, + } + _ = s.Releases.Upsert(ctx, release) + + job := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", ReleaseId: release.ID(), + Status: oapi.JobStatusPending, Metadata: map[string]string{}, + CreatedAt: time.Now(), UpdatedAt: time.Now(), + } + s.Jobs.Upsert(ctx, job) + + err := r.Dispatch(ctx, job) + + assert.NoError(t, err) + assert.Len(t, fake.dispatchedContexts, 1) + + dc := fake.dispatchedContexts[0] + assert.NotNil(t, dc.Release) + assert.NotNil(t, dc.Deployment) + assert.Equal(t, "dep-1", dc.Deployment.Id) + assert.NotNil(t, dc.Environment) + assert.Equal(t, "env-1", dc.Environment.Id) + assert.NotNil(t, dc.Resource) + assert.Equal(t, "res-1", dc.Resource.Id) + assert.Equal(t, "yes", dc.JobAgentConfig["deploy_cfg"]) +} + +func TestDispatch_VersionJobAgentConfigMergedViaFillReleaseContext(t *testing.T) { + ctx := context.Background() + s := newTestStore() + r, fake := newTestRegistry(s) + + agent := &oapi.JobAgent{ + Id: "agent-1", Name: "agent-1", Type: "fake", + Config: oapi.JobAgentConfig{"shared": "agent", "agent_only": "a"}, + } + s.JobAgents.Upsert(ctx, agent) + + env := &oapi.Environment{Id: "env-1", Name: "staging", SystemId: "sys-1"} + dep := &oapi.Deployment{ + Id: "dep-1", Name: "web", SystemId: "sys-1", + Metadata: map[string]string{}, + JobAgentConfig: oapi.JobAgentConfig{"shared": "deployment", "deploy_only": "d"}, + } + res := &oapi.Resource{ + Id: "res-1", Name: "node", Kind: "vm", Identifier: "node-1", + Version: "v1", WorkspaceId: "test-workspace", + Config: map[string]interface{}{}, Metadata: map[string]string{}, + CreatedAt: time.Now(), + } + s.Environments.Upsert(ctx, env) + s.Deployments.Upsert(ctx, dep) + s.Resources.Upsert(ctx, res) + + release := &oapi.Release{ + ReleaseTarget: oapi.ReleaseTarget{ + EnvironmentId: "env-1", + DeploymentId: "dep-1", + ResourceId: "res-1", + }, + Version: oapi.DeploymentVersion{ + Id: "ver-1", + Tag: "v3.0.0", + JobAgentConfig: oapi.JobAgentConfig{"shared": "version", "version_only": "v"}, + }, + Variables: map[string]oapi.LiteralValue{}, + } + _ = s.Releases.Upsert(ctx, release) + + job := &oapi.Job{ + Id: "job-1", JobAgentId: "agent-1", ReleaseId: release.ID(), + Status: oapi.JobStatusPending, Metadata: map[string]string{}, + CreatedAt: time.Now(), UpdatedAt: time.Now(), + } + s.Jobs.Upsert(ctx, job) + + err := r.Dispatch(ctx, job) + + assert.NoError(t, err) + assert.Len(t, fake.dispatchedContexts, 1) + + dc := fake.dispatchedContexts[0] + + // Version should be populated by fillReleaseContext, not manually set + assert.NotNil(t, dc.Version, "fillReleaseContext should populate DispatchContext.Version from the release") + assert.Equal(t, "ver-1", dc.Version.Id) + + // Version's JobAgentConfig should win the "shared" key (highest priority) + assert.Equal(t, "version", dc.JobAgentConfig["shared"], + "version JobAgentConfig should override agent and deployment configs") + assert.Equal(t, "a", dc.JobAgentConfig["agent_only"]) + assert.Equal(t, "d", dc.JobAgentConfig["deploy_only"]) + assert.Equal(t, "v", dc.JobAgentConfig["version_only"]) +} + +func TestRegister_AddsDispatcher(t *testing.T) { + s := newTestStore() + r := &Registry{ + dispatchers: make(map[string]types.Dispatchable), + store: s, + } + + fake := &fakeDispatcher{} + r.Register(fake) + + _, ok := r.dispatchers["fake"] + assert.True(t, ok) +} + +func TestRegister_OverwritesExistingType(t *testing.T) { + s := newTestStore() + r := &Registry{ + dispatchers: make(map[string]types.Dispatchable), + store: s, + } + + first := &fakeDispatcher{} + second := &fakeDispatcher{} + r.Register(first) + r.Register(second) + + assert.Same(t, second, r.dispatchers["fake"].(*fakeDispatcher)) +} diff --git a/apps/workspace-engine/pkg/workspace/jobagents/terraformcloud/tfe.go b/apps/workspace-engine/pkg/workspace/jobagents/terraformcloud/tfe.go index 8343f6f84..95cdc55df 100644 --- a/apps/workspace-engine/pkg/workspace/jobagents/terraformcloud/tfe.go +++ b/apps/workspace-engine/pkg/workspace/jobagents/terraformcloud/tfe.go @@ -20,13 +20,6 @@ func (t *TFE) Type() string { return "tfe" } -func (t *TFE) Supports() types.Capabilities { - return types.Capabilities{ - Workflows: false, - Deployments: true, - } -} - func (t *TFE) Dispatch(ctx context.Context, context types.DispatchContext) error { return nil } diff --git a/apps/workspace-engine/pkg/workspace/jobagents/testrunner/testrunner.go b/apps/workspace-engine/pkg/workspace/jobagents/testrunner/testrunner.go index 5eabeba56..1273a37ce 100644 --- a/apps/workspace-engine/pkg/workspace/jobagents/testrunner/testrunner.go +++ b/apps/workspace-engine/pkg/workspace/jobagents/testrunner/testrunner.go @@ -60,13 +60,6 @@ func (t *TestRunner) Type() string { return "test-runner" } -func (t *TestRunner) Supports() types.Capabilities { - return types.Capabilities{ - Workflows: true, - Deployments: true, - } -} - func (t *TestRunner) Dispatch(ctx context.Context, renderCtx types.DispatchContext) error { ctx, span := tracer.Start(ctx, "TestRunner.Dispatch") defer span.End() diff --git a/apps/workspace-engine/pkg/workspace/jobagents/types/types.go b/apps/workspace-engine/pkg/workspace/jobagents/types/types.go index 221a43258..2929e1551 100644 --- a/apps/workspace-engine/pkg/workspace/jobagents/types/types.go +++ b/apps/workspace-engine/pkg/workspace/jobagents/types/types.go @@ -9,12 +9,6 @@ import ( type Dispatchable interface { Type() string Dispatch(ctx context.Context, context DispatchContext) error - Supports() Capabilities -} - -type Capabilities struct { - Workflows bool - Deployments bool } type DispatchContext struct { diff --git a/apps/workspace-engine/pkg/workspace/jobs/factory.go b/apps/workspace-engine/pkg/workspace/jobs/factory.go index faebd92c8..8378965a9 100644 --- a/apps/workspace-engine/pkg/workspace/jobs/factory.go +++ b/apps/workspace-engine/pkg/workspace/jobs/factory.go @@ -3,7 +3,6 @@ package jobs import ( "context" - "encoding/json" "fmt" "time" "workspace-engine/pkg/oapi" @@ -30,41 +29,6 @@ func NewFactory(store *store.Store) *Factory { } } -func (f *Factory) MergeJobAgentConfig(deployment *oapi.Deployment, jobAgent *oapi.JobAgent, version *oapi.DeploymentVersion) (oapi.JobAgentConfig, error) { - deploymentConfig := deployment.JobAgentConfig - runnerConfig := jobAgent.Config - deploymentMap, err := toMap(deploymentConfig) - if err != nil { - return oapi.JobAgentConfig{}, fmt.Errorf("failed to convert deployment job agent config to map: %w", err) - } - runnerMap, err := toMap(runnerConfig) - if err != nil { - return oapi.JobAgentConfig{}, fmt.Errorf("failed to convert job agent config to map: %w", err) - } - versionMap, err := toMap(version.JobAgentConfig) - if err != nil { - return oapi.JobAgentConfig{}, fmt.Errorf("failed to convert deployment version job agent config to map: %w", err) - } - - // Merge job agent defaults first, then apply deployment overrides, then apply version overrides. - mergedConfig := make(map[string]any) - deepMerge(mergedConfig, runnerMap) - deepMerge(mergedConfig, deploymentMap) - deepMerge(mergedConfig, versionMap) - - mergedJSON, err := json.Marshal(mergedConfig) - if err != nil { - return oapi.JobAgentConfig{}, fmt.Errorf("failed to marshal merged job agent config: %w", err) - } - - var out oapi.JobAgentConfig - if err := json.Unmarshal(mergedJSON, &out); err != nil { - return oapi.JobAgentConfig{}, fmt.Errorf("failed to unmarshal merged job agent config: %w", err) - } - - return out, nil -} - // CreateJobForRelease creates a job for a given release (PURE FUNCTION, NO WRITES). // The job is configured with merged settings from JobAgent + Deployment. func (f *Factory) CreateJobForRelease(ctx context.Context, release *oapi.Release, action *trace.Action) (*oapi.Job, error) { @@ -152,32 +116,6 @@ func (f *Factory) CreateJobForRelease(ctx context.Context, release *oapi.Release AddMetadata("job_agent_type", jobAgent.Type) } - // Merge job agent config: deployment config overrides agent defaults - mergedConfig, err := f.MergeJobAgentConfig(deployment, jobAgent, &release.Version) - if err != nil { - if action != nil { - action.AddStep("Configure job", trace.StepResultFail, - fmt.Sprintf("Failed to merge job agent config: %v", err)). - AddMetadata("job_agent_id", *jobAgentId). - AddMetadata("deployment_id", deployment.Id). - AddMetadata("deployment_name", deployment.Name). - AddMetadata("issue", "invalid_job_agent_config") - } - // Create job with InvalidJobAgent status when config merge fails - msg := fmt.Sprintf("Failed to merge job agent config: %v", err) - return &oapi.Job{ - Id: uuid.New().String(), - ReleaseId: release.ID(), - JobAgentId: *jobAgentId, - JobAgentConfig: oapi.JobAgentConfig{}, - Status: oapi.JobStatusInvalidJobAgent, - Message: &msg, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Metadata: make(map[string]string), - }, nil - } - if action != nil { configMsg := "Applied default job agent configuration" @@ -200,7 +138,7 @@ func (f *Factory) CreateJobForRelease(ctx context.Context, release *oapi.Release Id: jobId, ReleaseId: release.ID(), JobAgentId: *jobAgentId, - JobAgentConfig: mergedConfig, + JobAgentConfig: oapi.JobAgentConfig{}, Status: oapi.JobStatusPending, CreatedAt: time.Now(), UpdatedAt: time.Now(), diff --git a/apps/workspace-engine/pkg/workspace/jobs/factory_test.go b/apps/workspace-engine/pkg/workspace/jobs/factory_test.go index ebf9aaba0..e9265961b 100644 --- a/apps/workspace-engine/pkg/workspace/jobs/factory_test.go +++ b/apps/workspace-engine/pkg/workspace/jobs/factory_test.go @@ -24,47 +24,6 @@ func mustCreateJobAgentConfig(t *testing.T, configJSON string) oapi.JobAgentConf return config } -// Helper functions for converting JobAgentConfig to specific typed configs -func getGithubJobAgentConfig(t *testing.T, config oapi.JobAgentConfig) *oapi.GithubJobAgentConfig { - t.Helper() - configJSON, err := json.Marshal(config) - require.NoError(t, err) - var fullConfig oapi.GithubJobAgentConfig - err = json.Unmarshal(configJSON, &fullConfig) - require.NoError(t, err) - return &fullConfig -} - -func getArgoCDJobAgentConfig(t *testing.T, config oapi.JobAgentConfig) *oapi.ArgoCDJobAgentConfig { - t.Helper() - configJSON, err := json.Marshal(config) - require.NoError(t, err) - var fullConfig oapi.ArgoCDJobAgentConfig - err = json.Unmarshal(configJSON, &fullConfig) - require.NoError(t, err) - return &fullConfig -} - -func getTerraformCloudJobAgentConfig(t *testing.T, config oapi.JobAgentConfig) *oapi.TerraformCloudJobAgentConfig { - t.Helper() - configJSON, err := json.Marshal(config) - require.NoError(t, err) - var fullConfig oapi.TerraformCloudJobAgentConfig - err = json.Unmarshal(configJSON, &fullConfig) - require.NoError(t, err) - return &fullConfig -} - -func getTestRunnerJobAgentConfig(t *testing.T, config oapi.JobAgentConfig) *oapi.TestRunnerJobAgentConfig { - t.Helper() - configJSON, err := json.Marshal(config) - require.NoError(t, err) - var fullConfig oapi.TestRunnerJobAgentConfig - err = json.Unmarshal(configJSON, &fullConfig) - require.NoError(t, err) - return &fullConfig -} - func mustCreateResourceSelector(t *testing.T) *oapi.Selector { t.Helper() selector := &oapi.Selector{} @@ -129,34 +88,17 @@ func createTestReleaseWithJobAgentConfig(t *testing.T, deploymentId, environment } // ============================================================================= -// GitHub App Config Merging Tests +// Error Cases // ============================================================================= -func TestFactory_MergeJobAgentConfig_GithubApp_BasicMerge(t *testing.T) { +func TestFactory_CreateJobForRelease_NoJobAgentConfigured(t *testing.T) { st := setupTestStore() ctx := context.Background() - jobAgentId := "agent-1" - - // JobAgent has installationId and owner (base config) - jobAgentConfig := oapi.JobAgentConfig{ - "type": "github-app", - "installationId": 12345, - "owner": "my-org", - } - - // Deployment has repo, workflowId, and ref (deployment overrides) - deploymentConfig := oapi.JobAgentConfig{ - "type": "github-app", - "repo": "my-repo", - "workflowId": 67890, - "ref": "main", - } - - jobAgent := createTestJobAgent(t, jobAgentId, "github-app", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) + // Deployment has no job agent configured + deploymentConfig := mustCreateJobAgentConfig(t, `{"type": "custom"}`) + deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - st.JobAgents.Upsert(ctx, jobAgent) _ = st.Deployments.Upsert(ctx, deployment) release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") @@ -164,51 +106,23 @@ func TestFactory_MergeJobAgentConfig_GithubApp_BasicMerge(t *testing.T) { factory := NewFactory(st) job, err := factory.CreateJobForRelease(ctx, release, nil) + // Should create a job with InvalidJobAgent status require.NoError(t, err) require.NotNil(t, job) - require.Equal(t, oapi.JobStatusPending, job.Status) - require.Equal(t, jobAgentId, job.JobAgentId) - - // Verify the merged config has all fields - fullConfig := getGithubJobAgentConfig(t, job.JobAgentConfig) - - // From JobAgent - require.Equal(t, 12345, fullConfig.InstallationId) - require.Equal(t, "my-org", fullConfig.Owner) - - // From Deployment - require.Equal(t, "my-repo", fullConfig.Repo) - require.Equal(t, int64(67890), fullConfig.WorkflowId) - require.NotNil(t, fullConfig.Ref) - require.Equal(t, "main", *fullConfig.Ref) - + require.Equal(t, oapi.JobStatusInvalidJobAgent, job.Status) + require.NotNil(t, job.Message) + require.Contains(t, *job.Message, "No job agent configured") } -func TestFactory_MergeJobAgentConfig_GithubApp_DeploymentOverridesRef(t *testing.T) { +func TestFactory_CreateJobForRelease_JobAgentNotFound(t *testing.T) { st := setupTestStore() ctx := context.Background() - jobAgentId := "agent-1" - - // JobAgent config (no ref) - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "github-app", - "installationId": 12345, - "owner": "my-org" - }`) - - // Deployment overrides with specific ref - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "github-app", - "repo": "my-repo", - "workflowId": 67890, - "ref": "feature-branch" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "github-app", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) + // Deployment references a job agent that doesn't exist + nonExistentAgentId := "non-existent-agent" + deploymentConfig := mustCreateJobAgentConfig(t, `{"type": "custom"}`) + deployment := createTestDeployment(t, "deploy-1", &nonExistentAgentId, deploymentConfig) - st.JobAgents.Upsert(ctx, jobAgent) _ = st.Deployments.Upsert(ctx, deployment) release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") @@ -216,95 +130,51 @@ func TestFactory_MergeJobAgentConfig_GithubApp_DeploymentOverridesRef(t *testing factory := NewFactory(st) job, err := factory.CreateJobForRelease(ctx, release, nil) + // Should create a job with InvalidJobAgent status require.NoError(t, err) require.NotNil(t, job) - - fullConfig := getGithubJobAgentConfig(t, job.JobAgentConfig) - - require.NotNil(t, fullConfig.Ref) - require.Equal(t, "feature-branch", *fullConfig.Ref) + require.Equal(t, oapi.JobStatusInvalidJobAgent, job.Status) + require.Equal(t, nonExistentAgentId, job.JobAgentId) + require.NotNil(t, job.Message) + require.Contains(t, *job.Message, "not found") } -// ============================================================================= -// ArgoCD Config Merging Tests -// ============================================================================= - -func TestFactory_MergeJobAgentConfig_ArgoCD_BasicMerge(t *testing.T) { +func TestFactory_CreateJobForRelease_DeploymentNotFound(t *testing.T) { st := setupTestStore() ctx := context.Background() - jobAgentId := "agent-1" - - // JobAgent has apiKey and serverUrl (base config) - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "argo-cd", - "apiKey": "secret-api-key", - "serverUrl": "https://argocd.example.com" - }`) - - // Deployment has type only (no template - template comes from version) - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "argo-cd" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "argo-cd", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - // Version has template (version override) - versionJobAgentConfig := map[string]interface{}{ - "type": "argo-cd", - "template": "apiVersion: argoproj.io/v1alpha1\nkind: Application\nmetadata:\n name: {{ .deployment.name }}", - } - release := createTestReleaseWithJobAgentConfig(t, "deploy-1", "env-1", "resource-1", "version-1", versionJobAgentConfig) + // Release references a deployment that doesn't exist + release := createTestRelease(t, "non-existent-deploy", "env-1", "resource-1", "version-1") factory := NewFactory(st) job, err := factory.CreateJobForRelease(ctx, release, nil) - require.NoError(t, err) - require.NotNil(t, job) - require.Equal(t, oapi.JobStatusPending, job.Status) - - // Verify the merged config has all fields - fullConfig := getArgoCDJobAgentConfig(t, job.JobAgentConfig) - - // From JobAgent - require.Equal(t, "secret-api-key", fullConfig.ApiKey) - require.Equal(t, "https://argocd.example.com", fullConfig.ServerUrl) - - // From Version - require.Contains(t, fullConfig.Template, "argoproj.io/v1alpha1") - require.Contains(t, fullConfig.Template, "{{ .deployment.name }}") - + // Should return an error + require.Error(t, err) + require.Nil(t, job) + require.Contains(t, err.Error(), "not found") } // ============================================================================= -// Terraform Cloud Config Merging Tests +// Job Creation Metadata Tests // ============================================================================= -func TestFactory_MergeJobAgentConfig_TerraformCloud_BasicMerge(t *testing.T) { +func TestFactory_CreateJobForRelease_SetsCorrectJobFields(t *testing.T) { st := setupTestStore() ctx := context.Background() jobAgentId := "agent-1" - // JobAgent has address, organization, token (base config) jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "tfe", - "address": "https://app.terraform.io", - "organization": "my-org", - "token": "secret-token" + "type": "custom", + "key": "value" }`) - // Deployment has template (deployment override) deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "tfe", - "template": "name: {{ .deployment.name }}\nworkingDirectory: /terraform" + "type": "custom" }`) - jobAgent := createTestJobAgent(t, jobAgentId, "tfe", jobAgentConfig) + jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) st.JobAgents.Upsert(ctx, jobAgent) @@ -312,95 +182,56 @@ func TestFactory_MergeJobAgentConfig_TerraformCloud_BasicMerge(t *testing.T) { release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") + beforeCreation := time.Now() + factory := NewFactory(st) job, err := factory.CreateJobForRelease(ctx, release, nil) + afterCreation := time.Now() + require.NoError(t, err) require.NotNil(t, job) - require.Equal(t, oapi.JobStatusPending, job.Status) - - // Verify the merged config has all fields - fullConfig := getTerraformCloudJobAgentConfig(t, job.JobAgentConfig) - - // From JobAgent - require.Equal(t, "https://app.terraform.io", fullConfig.Address) - require.Equal(t, "my-org", fullConfig.Organization) - require.Equal(t, "secret-token", fullConfig.Token) - - // From Deployment - require.Contains(t, fullConfig.Template, "{{ .deployment.name }}") - require.Contains(t, fullConfig.Template, "/terraform") - -} - -func TestFactory_MergeJobAgentConfig_TerraformCloud_DeploymentOverridesTemplate(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - // JobAgent has a default template - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "tfe", - "address": "https://app.terraform.io", - "organization": "my-org", - "token": "secret-token", - "template": "default-template" - }`) - - // Deployment overrides the template - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "tfe", - "template": "deployment-specific-template" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "tfe", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") + // Verify job ID is a valid UUID + _, err = uuid.Parse(job.Id) + require.NoError(t, err) - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) + // Verify release ID is correct + require.Equal(t, release.ID(), job.ReleaseId) - require.NoError(t, err) - require.NotNil(t, job) + // Verify job agent ID is correct + require.Equal(t, jobAgentId, job.JobAgentId) - fullConfig := getTerraformCloudJobAgentConfig(t, job.JobAgentConfig) + // Verify status is Pending + require.Equal(t, oapi.JobStatusPending, job.Status) - // Deployment template should override job agent template - require.Equal(t, "deployment-specific-template", fullConfig.Template) + // Verify timestamps are set correctly + require.True(t, job.CreatedAt.After(beforeCreation) || job.CreatedAt.Equal(beforeCreation)) + require.True(t, job.CreatedAt.Before(afterCreation) || job.CreatedAt.Equal(afterCreation)) + require.True(t, job.UpdatedAt.After(beforeCreation) || job.UpdatedAt.Equal(beforeCreation)) + require.True(t, job.UpdatedAt.Before(afterCreation) || job.UpdatedAt.Equal(afterCreation)) - // But other fields from JobAgent should still be present - require.Equal(t, "https://app.terraform.io", fullConfig.Address) - require.Equal(t, "my-org", fullConfig.Organization) - require.Equal(t, "secret-token", fullConfig.Token) + // Verify metadata is initialized + require.NotNil(t, job.Metadata) + require.Empty(t, job.Metadata) } // ============================================================================= -// Custom Config Merging Tests +// Multiple Jobs Creation Tests // ============================================================================= -func TestFactory_MergeJobAgentConfig_Custom_BasicMerge(t *testing.T) { +func TestFactory_CreateJobForRelease_UniqueJobIds(t *testing.T) { st := setupTestStore() ctx := context.Background() jobAgentId := "agent-1" - // JobAgent has some custom properties jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "baseUrl": "https://api.example.com", - "timeout": 30 + "type": "custom" }`) - // Deployment has additional custom properties deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "endpoint": "/deploy", - "retries": 3 + "type": "custom" }`) jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) @@ -409,118 +240,35 @@ func TestFactory_MergeJobAgentConfig_Custom_BasicMerge(t *testing.T) { st.JobAgents.Upsert(ctx, jobAgent) _ = st.Deployments.Upsert(ctx, deployment) - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - require.Equal(t, oapi.JobStatusPending, job.Status) - - // Verify the merged config has all fields by parsing as JSON - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // From JobAgent - require.Equal(t, "https://api.example.com", configMap["baseUrl"]) - require.Equal(t, float64(30), configMap["timeout"]) - // From Deployment - require.Equal(t, "/deploy", configMap["endpoint"]) - require.Equal(t, float64(3), configMap["retries"]) + // Create multiple jobs for the same release + jobIds := make(map[string]bool) + for i := 0; i < 10; i++ { + release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") + job, err := factory.CreateJobForRelease(ctx, release, nil) + require.NoError(t, err) + require.NotNil(t, job) - // Type discriminator should be set - require.Equal(t, "custom", configMap["type"]) + // Each job should have a unique ID + require.False(t, jobIds[job.Id], "Job ID should be unique") + jobIds[job.Id] = true + } } -func TestFactory_MergeJobAgentConfig_Custom_DeploymentOverridesValues(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - // JobAgent has default values - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "baseUrl": "https://api.example.com", - "timeout": 30, - "env": "production" - }`) - - // Deployment overrides some values - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "timeout": 60, - "env": "staging" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // BaseUrl from JobAgent (not overridden) - require.Equal(t, "https://api.example.com", configMap["baseUrl"]) - - // Deployment overrides - require.Equal(t, float64(60), configMap["timeout"]) - require.Equal(t, "staging", configMap["env"]) -} +// ============================================================================= +// Empty Job Agent ID Tests +// ============================================================================= -func TestFactory_MergeJobAgentConfig_Custom_DeepNestedMerge(t *testing.T) { +func TestFactory_CreateJobForRelease_EmptyJobAgentId(t *testing.T) { st := setupTestStore() ctx := context.Background() - jobAgentId := "agent-1" - - // JobAgent has nested config - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "settings": { - "debug": false, - "logging": { - "level": "info", - "format": "json" - } - } - }`) - - // Deployment overrides nested values - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "settings": { - "debug": true, - "logging": { - "level": "debug" - } - } - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) + // Deployment has empty string job agent ID + emptyAgentId := "" + deploymentConfig := mustCreateJobAgentConfig(t, `{"type": "custom"}`) + deployment := createTestDeployment(t, "deploy-1", &emptyAgentId, deploymentConfig) - st.JobAgents.Upsert(ctx, jobAgent) _ = st.Deployments.Upsert(ctx, deployment) release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") @@ -528,1885 +276,10 @@ func TestFactory_MergeJobAgentConfig_Custom_DeepNestedMerge(t *testing.T) { factory := NewFactory(st) job, err := factory.CreateJobForRelease(ctx, release, nil) - require.NoError(t, err) - require.NotNil(t, job) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - settings := configMap["settings"].(map[string]any) - - // debug should be overridden to true - require.Equal(t, true, settings["debug"]) - - logging := settings["logging"].(map[string]any) - - // level should be overridden to "debug" - require.Equal(t, "debug", logging["level"]) - - // format should be preserved from JobAgent (deep merge) - require.Equal(t, "json", logging["format"]) -} - -// ============================================================================= -// Error Cases -// ============================================================================= - -func TestFactory_CreateJobForRelease_NoJobAgentConfigured(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - // Deployment has no job agent configured - deploymentConfig := mustCreateJobAgentConfig(t, `{"type": "custom"}`) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - // Should create a job with InvalidJobAgent status - require.NoError(t, err) - require.NotNil(t, job) - require.Equal(t, oapi.JobStatusInvalidJobAgent, job.Status) - require.NotNil(t, job.Message) - require.Contains(t, *job.Message, "No job agent configured") -} - -func TestFactory_CreateJobForRelease_JobAgentNotFound(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - // Deployment references a job agent that doesn't exist - nonExistentAgentId := "non-existent-agent" - deploymentConfig := mustCreateJobAgentConfig(t, `{"type": "custom"}`) - deployment := createTestDeployment(t, "deploy-1", &nonExistentAgentId, deploymentConfig) - - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - // Should create a job with InvalidJobAgent status - require.NoError(t, err) - require.NotNil(t, job) - require.Equal(t, oapi.JobStatusInvalidJobAgent, job.Status) - require.Equal(t, nonExistentAgentId, job.JobAgentId) - require.NotNil(t, job.Message) - require.Contains(t, *job.Message, "not found") -} - -func TestFactory_CreateJobForRelease_DeploymentNotFound(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - // Release references a deployment that doesn't exist - release := createTestRelease(t, "non-existent-deploy", "env-1", "resource-1", "version-1") - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - // Should return an error - require.Error(t, err) - require.Nil(t, job) - require.Contains(t, err.Error(), "not found") -} - -// ============================================================================= -// Test Runner Config Tests (No deployment override - agent only) -// ============================================================================= - -func TestFactory_MergeJobAgentConfig_TestRunner_PassthroughConfig(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - // TestRunner config on JobAgent - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "test-runner", - "delaySeconds": 5, - "status": "completed", - "message": "Deployment completed" - }`) - - // Deployment config with test-runner type - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "test-runner" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "test-runner", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - require.Equal(t, oapi.JobStatusPending, job.Status) - - // Verify the config has test-runner fields from JobAgent - fullConfig := getTestRunnerJobAgentConfig(t, job.JobAgentConfig) - - require.NotNil(t, fullConfig.DelaySeconds) - require.Equal(t, 5, *fullConfig.DelaySeconds) - require.NotNil(t, fullConfig.Status) - require.Equal(t, "completed", *fullConfig.Status) - require.NotNil(t, fullConfig.Message) - require.Equal(t, "Deployment completed", *fullConfig.Message) -} - -// ============================================================================= -// Job Creation Metadata Tests -// ============================================================================= - -func TestFactory_CreateJobForRelease_SetsCorrectJobFields(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "key": "value" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - - beforeCreation := time.Now() - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - afterCreation := time.Now() - - require.NoError(t, err) - require.NotNil(t, job) - - // Verify job ID is a valid UUID - _, err = uuid.Parse(job.Id) - require.NoError(t, err) - - // Verify release ID is correct - require.Equal(t, release.ID(), job.ReleaseId) - - // Verify job agent ID is correct - require.Equal(t, jobAgentId, job.JobAgentId) - - // Verify status is Pending - require.Equal(t, oapi.JobStatusPending, job.Status) - - // Verify timestamps are set correctly - require.True(t, job.CreatedAt.After(beforeCreation) || job.CreatedAt.Equal(beforeCreation)) - require.True(t, job.CreatedAt.Before(afterCreation) || job.CreatedAt.Equal(afterCreation)) - require.True(t, job.UpdatedAt.After(beforeCreation) || job.UpdatedAt.Equal(beforeCreation)) - require.True(t, job.UpdatedAt.Before(afterCreation) || job.UpdatedAt.Equal(afterCreation)) - - // Verify metadata is initialized - require.NotNil(t, job.Metadata) - require.Empty(t, job.Metadata) -} - -// ============================================================================= -// Multiple Jobs Creation Tests -// ============================================================================= - -func TestFactory_CreateJobForRelease_UniqueJobIds(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - factory := NewFactory(st) - - // Create multiple jobs for the same release - jobIds := make(map[string]bool) - for i := 0; i < 10; i++ { - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - job, err := factory.CreateJobForRelease(ctx, release, nil) - require.NoError(t, err) - require.NotNil(t, job) - - // Each job should have a unique ID - require.False(t, jobIds[job.Id], "Job ID should be unique") - jobIds[job.Id] = true - } -} - -// ============================================================================= -// Empty Job Agent ID Tests -// ============================================================================= - -func TestFactory_CreateJobForRelease_EmptyJobAgentId(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - // Deployment has empty string job agent ID - emptyAgentId := "" - deploymentConfig := mustCreateJobAgentConfig(t, `{"type": "custom"}`) - deployment := createTestDeployment(t, "deploy-1", &emptyAgentId, deploymentConfig) - - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - // Should create a job with InvalidJobAgent status + // Should create a job with InvalidJobAgent status require.NoError(t, err) require.NotNil(t, job) require.Equal(t, oapi.JobStatusInvalidJobAgent, job.Status) require.NotNil(t, job.Message) require.Contains(t, *job.Message, "No job agent configured") } - -// ============================================================================= -// Version JobAgentConfig Override Tests -// ============================================================================= - -func TestFactory_MergeJobAgentConfig_VersionOverridesDeployment(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "baseUrl": "https://api.example.com" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "timeout": 30, - "env": "production" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - versionJobAgentConfig := map[string]interface{}{ - "type": "custom", - "timeout": 60, - "env": "staging", - } - release := createTestReleaseWithJobAgentConfig(t, "deploy-1", "env-1", "resource-1", "version-1", versionJobAgentConfig) - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - require.Equal(t, oapi.JobStatusPending, job.Status) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - require.Equal(t, "https://api.example.com", configMap["baseUrl"]) - require.Equal(t, float64(60), configMap["timeout"]) - require.Equal(t, "staging", configMap["env"]) -} - -func TestFactory_MergeJobAgentConfig_VersionAddsNewFields(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "baseUrl": "https://api.example.com" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "timeout": 30 - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - versionJobAgentConfig := map[string]interface{}{ - "type": "custom", - "versionId": "v1.2.3", - "extra": "field", - } - release := createTestReleaseWithJobAgentConfig(t, "deploy-1", "env-1", "resource-1", "version-1", versionJobAgentConfig) - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - require.Equal(t, "https://api.example.com", configMap["baseUrl"]) - require.Equal(t, float64(30), configMap["timeout"]) - require.Equal(t, "v1.2.3", configMap["versionId"]) - require.Equal(t, "field", configMap["extra"]) -} - -func TestFactory_MergeJobAgentConfig_EmptyVersionConfig_IsNoop(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "baseUrl": "https://api.example.com" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "timeout": 30, - "env": "production" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - require.Equal(t, oapi.JobStatusPending, job.Status) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - require.Equal(t, "https://api.example.com", configMap["baseUrl"]) - require.Equal(t, float64(30), configMap["timeout"]) - require.Equal(t, "production", configMap["env"]) -} - -func TestFactory_MergeJobAgentConfig_NilVersionConfig_IsNoop(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "baseUrl": "https://api.example.com" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "timeout": 30 - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestReleaseWithJobAgentConfig(t, "deploy-1", "env-1", "resource-1", "version-1", nil) - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - require.Equal(t, oapi.JobStatusPending, job.Status) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - require.Equal(t, "https://api.example.com", configMap["baseUrl"]) - require.Equal(t, float64(30), configMap["timeout"]) -} - -func TestFactory_MergeJobAgentConfig_VersionDeepNestedOverride(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "settings": { - "debug": false, - "logging": { - "level": "info", - "format": "json" - } - } - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "settings": { - "debug": true, - "logging": { - "level": "debug" - } - } - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - versionJobAgentConfig := map[string]interface{}{ - "type": "custom", - "settings": map[string]interface{}{ - "logging": map[string]interface{}{ - "level": "warn", - }, - }, - } - release := createTestReleaseWithJobAgentConfig(t, "deploy-1", "env-1", "resource-1", "version-1", versionJobAgentConfig) - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - settings := configMap["settings"].(map[string]any) - require.Equal(t, true, settings["debug"]) - - logging := settings["logging"].(map[string]any) - require.Equal(t, "warn", logging["level"]) - require.Equal(t, "json", logging["format"]) -} - -func TestFactory_MergeJobAgentConfig_VersionOverridesAll_GithubApp(t *testing.T) { - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "github-app", - "installationId": 12345, - "owner": "my-org" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "github-app", - "repo": "my-repo", - "workflowId": 67890, - "ref": "main" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "github-app", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - versionJobAgentConfig := map[string]interface{}{ - "type": "github-app", - "ref": "release-v2", - } - release := createTestReleaseWithJobAgentConfig(t, "deploy-1", "env-1", "resource-1", "version-1", versionJobAgentConfig) - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - require.Equal(t, oapi.JobStatusPending, job.Status) - - fullConfig := getGithubJobAgentConfig(t, job.JobAgentConfig) - - require.Equal(t, 12345, fullConfig.InstallationId) - require.Equal(t, "my-org", fullConfig.Owner) - require.Equal(t, "my-repo", fullConfig.Repo) - require.Equal(t, int64(67890), fullConfig.WorkflowId) - require.NotNil(t, fullConfig.Ref) - require.Equal(t, "release-v2", *fullConfig.Ref) -} - -// ============================================================================= -// JobAgentConfig Merge Ordering Tests -// ============================================================================= -// These tests verify that the merge order is correct: -// 1. JobAgent config (base defaults) -// 2. Deployment config (deployment-specific overrides) -// 3. Version config (version-specific overrides) -// Each layer can override values from previous layers, and deep nested -// objects are merged recursively. - -func TestFactory_MergeJobAgentConfig_ThreeLevelMergeOrder(t *testing.T) { - // This test verifies the complete 3-level merge: - // JobAgent -> Deployment -> Version - // Each level can add new fields or override existing ones. - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - // Level 1: JobAgent provides base config with some defaults - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "agentOnly": "from-agent", - "sharedField": "agent-value", - "overriddenByDeployment": "agent-value", - "overriddenByVersion": "agent-value", - "overriddenByBoth": "agent-value" - }`) - - // Level 2: Deployment adds new fields and overrides some - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "deploymentOnly": "from-deployment", - "overriddenByDeployment": "deployment-value", - "overriddenByVersion": "deployment-value", - "overriddenByBoth": "deployment-value" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - // Level 3: Version adds new fields and overrides some - versionJobAgentConfig := map[string]interface{}{ - "type": "custom", - "versionOnly": "from-version", - "overriddenByVersion": "version-value", - "overriddenByBoth": "version-value", - } - release := createTestReleaseWithJobAgentConfig(t, "deploy-1", "env-1", "resource-1", "version-1", versionJobAgentConfig) - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - require.Equal(t, oapi.JobStatusPending, job.Status) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // Fields unique to each level should be preserved - require.Equal(t, "from-agent", configMap["agentOnly"], "agentOnly should come from JobAgent") - require.Equal(t, "from-deployment", configMap["deploymentOnly"], "deploymentOnly should come from Deployment") - require.Equal(t, "from-version", configMap["versionOnly"], "versionOnly should come from Version") - - // Shared field not overridden should stay from JobAgent - require.Equal(t, "agent-value", configMap["sharedField"], "sharedField should remain from JobAgent") - - // Fields overridden at different levels - require.Equal(t, "deployment-value", configMap["overriddenByDeployment"], "overriddenByDeployment should come from Deployment") - require.Equal(t, "version-value", configMap["overriddenByVersion"], "overriddenByVersion should come from Version") - require.Equal(t, "version-value", configMap["overriddenByBoth"], "overriddenByBoth should come from Version (last wins)") - - // Type discriminator should always be present - require.Equal(t, "custom", configMap["type"]) -} - -func TestFactory_MergeJobAgentConfig_DeepNestedThreeLevelMerge(t *testing.T) { - // This test verifies that deep nested objects are merged correctly - // at all three levels. - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - // Level 1: JobAgent with deep nested config - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "settings": { - "agent": { - "onlyInAgent": "agent-value" - }, - "shared": { - "level1": { - "fromAgent": "agent", - "overrideByDeployment": "agent", - "overrideByVersion": "agent" - } - } - } - }`) - - // Level 2: Deployment adds to nested structure and overrides some - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "settings": { - "deployment": { - "onlyInDeployment": "deployment-value" - }, - "shared": { - "level1": { - "fromDeployment": "deployment", - "overrideByDeployment": "deployment", - "overrideByVersion": "deployment" - } - } - } - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - // Level 3: Version adds more nested structure and overrides - versionJobAgentConfig := map[string]interface{}{ - "type": "custom", - "settings": map[string]interface{}{ - "version": map[string]interface{}{ - "onlyInVersion": "version-value", - }, - "shared": map[string]interface{}{ - "level1": map[string]interface{}{ - "fromVersion": "version", - "overrideByVersion": "version", - }, - }, - }, - } - release := createTestReleaseWithJobAgentConfig(t, "deploy-1", "env-1", "resource-1", "version-1", versionJobAgentConfig) - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - settings := configMap["settings"].(map[string]any) - - // Each level's unique nested object should be preserved - agent := settings["agent"].(map[string]any) - require.Equal(t, "agent-value", agent["onlyInAgent"], "Agent-only nested field should be preserved") - - deployment2 := settings["deployment"].(map[string]any) - require.Equal(t, "deployment-value", deployment2["onlyInDeployment"], "Deployment-only nested field should be preserved") - - version := settings["version"].(map[string]any) - require.Equal(t, "version-value", version["onlyInVersion"], "Version-only nested field should be preserved") - - // Verify deep merge in shared object - shared := settings["shared"].(map[string]any) - level1 := shared["level1"].(map[string]any) - - require.Equal(t, "agent", level1["fromAgent"], "Agent field in deep nest should be preserved") - require.Equal(t, "deployment", level1["fromDeployment"], "Deployment field in deep nest should be preserved") - require.Equal(t, "version", level1["fromVersion"], "Version field in deep nest should be preserved") - require.Equal(t, "deployment", level1["overrideByDeployment"], "Deployment should override agent in deep nest") - require.Equal(t, "version", level1["overrideByVersion"], "Version should override deployment in deep nest") -} - -func TestFactory_MergeJobAgentConfig_VersionOverridesNull(t *testing.T) { - // This test verifies that version can override values to null - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "field": "agent-value" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "field": "deployment-value", - "extra": "extra-value" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - // Version explicitly sets field to null - versionJobAgentConfig := map[string]interface{}{ - "type": "custom", - "field": nil, - } - release := createTestReleaseWithJobAgentConfig(t, "deploy-1", "env-1", "resource-1", "version-1", versionJobAgentConfig) - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // Field should be overridden to null - require.Nil(t, configMap["field"], "field should be nil when version sets it to null") - // Extra field from deployment should still be present - require.Equal(t, "extra-value", configMap["extra"]) -} - -func TestFactory_MergeJobAgentConfig_ArraysNotDeepMerged(t *testing.T) { - // This test verifies that arrays are replaced, not merged - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "items": ["agent-item-1", "agent-item-2"] - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "items": ["deployment-item-1"] - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // Arrays should be replaced, not merged - items := configMap["items"].([]any) - require.Len(t, items, 1, "Array should be replaced, not merged") - require.Equal(t, "deployment-item-1", items[0], "Array should contain deployment value") -} - -func TestFactory_MergeJobAgentConfig_VersionArrayOverride(t *testing.T) { - // This test verifies that version can override arrays - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "tags": ["base-tag"] - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "tags": ["deployment-tag-1", "deployment-tag-2"] - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - versionJobAgentConfig := map[string]interface{}{ - "type": "custom", - "tags": []string{"version-tag"}, - } - release := createTestReleaseWithJobAgentConfig(t, "deploy-1", "env-1", "resource-1", "version-1", versionJobAgentConfig) - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // Version array should completely replace deployment array - tags := configMap["tags"].([]any) - require.Len(t, tags, 1) - require.Equal(t, "version-tag", tags[0]) -} - -func TestFactory_MergeJobAgentConfig_PreservesTypeDiscriminator(t *testing.T) { - // This test verifies that the type discriminator is always set - // to the JobAgent's type, regardless of what deployment/version specify - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - // JobAgent has custom type - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "field": "value" - }`) - - // Deployment might try to set a different type (though shouldn't) - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // Type discriminator must always be from JobAgent - require.Equal(t, "custom", configMap["type"]) -} - -func TestFactory_MergeJobAgentConfig_EmptyDeploymentConfig(t *testing.T) { - // This test verifies that an empty deployment config doesn't - // affect the agent config - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "baseUrl": "https://api.example.com", - "timeout": 30, - "nested": { - "key": "value" - } - }`) - - // Empty deployment config (just type) - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // All agent config should be preserved - require.Equal(t, "https://api.example.com", configMap["baseUrl"]) - require.Equal(t, float64(30), configMap["timeout"]) - nested := configMap["nested"].(map[string]any) - require.Equal(t, "value", nested["key"]) -} - -func TestFactory_MergeJobAgentConfig_AllThreeLevelsEmpty(t *testing.T) { - // This test verifies behavior when all configs are minimal - st := setupTestStore() - ctx := context.Background() - - jobAgentId := "agent-1" - - // Minimal configs at all levels - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom" - }`) - - jobAgent := createTestJobAgent(t, jobAgentId, "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", &jobAgentId, deploymentConfig) - - st.JobAgents.Upsert(ctx, jobAgent) - _ = st.Deployments.Upsert(ctx, deployment) - - release := createTestRelease(t, "deploy-1", "env-1", "resource-1", "version-1") - - factory := NewFactory(st) - job, err := factory.CreateJobForRelease(ctx, release, nil) - - require.NoError(t, err) - require.NotNil(t, job) - require.Equal(t, oapi.JobStatusPending, job.Status) - - configJSON, err := json.Marshal(job.JobAgentConfig) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // Should only have the type discriminator - require.Equal(t, "custom", configMap["type"]) -} - -// ============================================================================= -// MergeJobAgentConfig Direct Unit Tests -// ============================================================================= -// These tests call MergeJobAgentConfig directly to test the merge logic -// in isolation, without going through CreateJobForRelease. - -func createTestVersion(t *testing.T, deploymentId string, jobAgentConfig map[string]interface{}) *oapi.DeploymentVersion { - t.Helper() - return &oapi.DeploymentVersion{ - Id: "version-1", - Tag: "v1.0.0", - DeploymentId: deploymentId, - Config: map[string]interface{}{}, - Metadata: map[string]string{}, - CreatedAt: time.Now(), - JobAgentConfig: jobAgentConfig, - } -} - -func TestMergeJobAgentConfig_BasicMerge_Custom(t *testing.T) { - factory := NewFactory(nil) // Store not needed for direct merge tests - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "agentField": "agent-value", - "shared": "from-agent" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "deploymentField": "deployment-value", - "shared": "from-deployment" - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", map[string]interface{}{ - "type": "custom", - "versionField": "version-value", - "shared": "from-version", - }) - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - configJSON, err := json.Marshal(result) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // Each level's unique field should be present - require.Equal(t, "agent-value", configMap["agentField"]) - require.Equal(t, "deployment-value", configMap["deploymentField"]) - require.Equal(t, "version-value", configMap["versionField"]) - - // Shared field should have version's value (last wins) - require.Equal(t, "from-version", configMap["shared"]) - - // Type discriminator should be set - require.Equal(t, "custom", configMap["type"]) -} - -func TestMergeJobAgentConfig_MergeOrder_VersionOverridesDeploymentOverridesAgent(t *testing.T) { - factory := NewFactory(nil) - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "level1": "agent", - "level2": "agent", - "level3": "agent" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "level2": "deployment", - "level3": "deployment" - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", map[string]interface{}{ - "type": "custom", - "level3": "version", - }) - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - configJSON, err := json.Marshal(result) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // level1: only in agent, should be "agent" - require.Equal(t, "agent", configMap["level1"], "level1 should come from agent (not overridden)") - - // level2: agent + deployment, should be "deployment" - require.Equal(t, "deployment", configMap["level2"], "level2 should come from deployment (overrides agent)") - - // level3: agent + deployment + version, should be "version" - require.Equal(t, "version", configMap["level3"], "level3 should come from version (overrides deployment)") -} - -func TestMergeJobAgentConfig_DeepMerge_NestedObjects(t *testing.T) { - factory := NewFactory(nil) - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "settings": { - "agent": { - "key": "agent-value" - }, - "shared": { - "fromAgent": true, - "override": "agent" - } - } - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "settings": { - "deployment": { - "key": "deployment-value" - }, - "shared": { - "fromDeployment": true, - "override": "deployment" - } - } - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", map[string]interface{}{ - "type": "custom", - "settings": map[string]interface{}{ - "version": map[string]interface{}{ - "key": "version-value", - }, - "shared": map[string]interface{}{ - "fromVersion": true, - "override": "version", - }, - }, - }) - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - configJSON, err := json.Marshal(result) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - settings := configMap["settings"].(map[string]any) - - // Each level's nested object should be preserved - agent := settings["agent"].(map[string]any) - require.Equal(t, "agent-value", agent["key"]) - - deploymentSettings := settings["deployment"].(map[string]any) - require.Equal(t, "deployment-value", deploymentSettings["key"]) - - versionSettings := settings["version"].(map[string]any) - require.Equal(t, "version-value", versionSettings["key"]) - - // Shared nested object should have all keys merged - shared := settings["shared"].(map[string]any) - require.Equal(t, true, shared["fromAgent"], "fromAgent should be preserved from agent") - require.Equal(t, true, shared["fromDeployment"], "fromDeployment should be preserved from deployment") - require.Equal(t, true, shared["fromVersion"], "fromVersion should be preserved from version") - require.Equal(t, "version", shared["override"], "override should be from version (last wins)") -} - -func TestMergeJobAgentConfig_NilVersionConfig(t *testing.T) { - factory := NewFactory(nil) - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "field": "agent-value" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "field": "deployment-value" - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", nil) // nil version config - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - configJSON, err := json.Marshal(result) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // Should have deployment value since version is nil - require.Equal(t, "deployment-value", configMap["field"]) -} - -func TestMergeJobAgentConfig_EmptyVersionConfig(t *testing.T) { - factory := NewFactory(nil) - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "field": "agent-value" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "field": "deployment-value" - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", map[string]interface{}{}) // empty map - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - configJSON, err := json.Marshal(result) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // Should have deployment value since version is empty - require.Equal(t, "deployment-value", configMap["field"]) -} - -func TestMergeJobAgentConfig_GithubApp_FullMerge(t *testing.T) { - factory := NewFactory(nil) - - // JobAgent provides installationId and owner - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "github-app", - "installationId": 12345, - "owner": "my-org" - }`) - - // Deployment provides repo, workflowId, and ref - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "github-app", - "repo": "my-repo", - "workflowId": 67890, - "ref": "main" - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "github-app", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", map[string]interface{}{ - "type": "github-app", - "ref": "release-v2", // Version overrides ref - }) - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - fullConfig := getGithubJobAgentConfig(t, result) - - // From JobAgent - require.Equal(t, 12345, fullConfig.InstallationId) - require.Equal(t, "my-org", fullConfig.Owner) - - // From Deployment - require.Equal(t, "my-repo", fullConfig.Repo) - require.Equal(t, int64(67890), fullConfig.WorkflowId) - - // From Version (overrides deployment) - require.NotNil(t, fullConfig.Ref) - require.Equal(t, "release-v2", *fullConfig.Ref) - -} - -func TestMergeJobAgentConfig_ArgoCD_SkipsDeploymentConfig(t *testing.T) { - // ArgoCD type skips deployment config (only uses agent + version) - factory := NewFactory(nil) - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "argo-cd", - "apiKey": "secret-key", - "serverUrl": "https://argocd.example.com" - }`) - - // This deployment config should be ignored for argo-cd type - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "argo-cd" - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "argo-cd", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", map[string]interface{}{ - "type": "argo-cd", - "template": "apiVersion: argoproj.io/v1alpha1\nkind: Application", - }) - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - fullConfig := getArgoCDJobAgentConfig(t, result) - - // From JobAgent - require.Equal(t, "secret-key", fullConfig.ApiKey) - require.Equal(t, "https://argocd.example.com", fullConfig.ServerUrl) - - // From Version - require.Contains(t, fullConfig.Template, "argoproj.io/v1alpha1") -} - -func TestMergeJobAgentConfig_TestRunner_SkipsDeploymentConfig(t *testing.T) { - // TestRunner type skips deployment config (only uses agent + version) - factory := NewFactory(nil) - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "test-runner", - "delaySeconds": 5, - "status": "completed" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "test-runner" - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "test-runner", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", map[string]interface{}{ - "type": "test-runner", - "message": "Custom message from version", - }) - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - fullConfig := getTestRunnerJobAgentConfig(t, result) - - // From JobAgent - require.NotNil(t, fullConfig.DelaySeconds) - require.Equal(t, 5, *fullConfig.DelaySeconds) - require.NotNil(t, fullConfig.Status) - require.Equal(t, "completed", *fullConfig.Status) - - // From Version - require.NotNil(t, fullConfig.Message) - require.Equal(t, "Custom message from version", *fullConfig.Message) -} - -func TestMergeJobAgentConfig_TerraformCloud_FullMerge(t *testing.T) { - factory := NewFactory(nil) - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "tfe", - "address": "https://app.terraform.io", - "organization": "my-org", - "token": "secret-token" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "tfe", - "template": "default-template" - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "tfe", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", map[string]interface{}{ - "type": "tfe", - "template": "version-template", // Override deployment template - }) - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - fullConfig := getTerraformCloudJobAgentConfig(t, result) - - // From JobAgent - require.Equal(t, "https://app.terraform.io", fullConfig.Address) - require.Equal(t, "my-org", fullConfig.Organization) - require.Equal(t, "secret-token", fullConfig.Token) - - // From Version (overrides deployment) - require.Equal(t, "version-template", fullConfig.Template) -} - -func TestMergeJobAgentConfig_ArraysAreReplaced(t *testing.T) { - factory := NewFactory(nil) - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "tags": ["agent-tag-1", "agent-tag-2"] - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "tags": ["deployment-tag"] - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", nil) - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - configJSON, err := json.Marshal(result) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // Arrays should be replaced, not merged - tags := configMap["tags"].([]any) - require.Len(t, tags, 1, "Arrays should be replaced, not merged") - require.Equal(t, "deployment-tag", tags[0]) -} - -func TestMergeJobAgentConfig_VersionArrayOverridesDeployment(t *testing.T) { - factory := NewFactory(nil) - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "tags": ["agent-tag"] - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "tags": ["deployment-tag-1", "deployment-tag-2"] - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", map[string]interface{}{ - "type": "custom", - "tags": []string{"version-tag"}, - }) - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - configJSON, err := json.Marshal(result) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - tags := configMap["tags"].([]any) - require.Len(t, tags, 1) - require.Equal(t, "version-tag", tags[0]) -} - -func TestMergeJobAgentConfig_DeeplyNestedOverride(t *testing.T) { - factory := NewFactory(nil) - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "level1": { - "level2": { - "level3": { - "fromAgent": "agent", - "override": "agent" - } - } - } - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "level1": { - "level2": { - "level3": { - "fromDeployment": "deployment", - "override": "deployment" - } - } - } - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", map[string]interface{}{ - "type": "custom", - "level1": map[string]interface{}{ - "level2": map[string]interface{}{ - "level3": map[string]interface{}{ - "fromVersion": "version", - "override": "version", - }, - }, - }, - }) - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - configJSON, err := json.Marshal(result) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - level1 := configMap["level1"].(map[string]any) - level2 := level1["level2"].(map[string]any) - level3 := level2["level3"].(map[string]any) - - // All sources should be merged - require.Equal(t, "agent", level3["fromAgent"]) - require.Equal(t, "deployment", level3["fromDeployment"]) - require.Equal(t, "version", level3["fromVersion"]) - - // Override should be from version (last wins) - require.Equal(t, "version", level3["override"]) -} - -func TestMergeJobAgentConfig_NullOverridesValue(t *testing.T) { - factory := NewFactory(nil) - - jobAgentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom", - "field": "agent-value", - "keepThis": "keep" - }`) - - deploymentConfig := mustCreateJobAgentConfig(t, `{ - "type": "custom" - }`) - - jobAgent := createTestJobAgent(t, "agent-1", "custom", jobAgentConfig) - deployment := createTestDeployment(t, "deploy-1", nil, deploymentConfig) - version := createTestVersion(t, "deploy-1", map[string]interface{}{ - "type": "custom", - "field": nil, // Explicitly set to nil - }) - - result, err := factory.MergeJobAgentConfig(deployment, jobAgent, version) - require.NoError(t, err) - - configJSON, err := json.Marshal(result) - require.NoError(t, err) - - var configMap map[string]any - err = json.Unmarshal(configJSON, &configMap) - require.NoError(t, err) - - // Field should be nil (overridden by version) - require.Nil(t, configMap["field"]) - - // Other fields should be preserved - require.Equal(t, "keep", configMap["keepThis"]) -} - -// ============================================================================= -// toMap Function Unit Tests -// ============================================================================= - -func TestToMap_NilInput(t *testing.T) { - result, err := toMap(nil) - require.NoError(t, err) - require.NotNil(t, result) - require.Empty(t, result) -} - -func TestToMap_MapStringAnyInput(t *testing.T) { - input := map[string]any{ - "key1": "value1", - "key2": 42, - "key3": true, - } - - result, err := toMap(input) - require.NoError(t, err) - require.Equal(t, input, result) -} - -func TestToMap_EmptyMapInput(t *testing.T) { - input := map[string]any{} - - result, err := toMap(input) - require.NoError(t, err) - require.NotNil(t, result) - require.Empty(t, result) -} - -func TestToMap_StructInput(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - input := testStruct{ - Name: "test", - Value: 123, - } - - result, err := toMap(input) - require.NoError(t, err) - require.Equal(t, "test", result["name"]) - require.Equal(t, float64(123), result["value"]) // JSON numbers become float64 -} - -func TestToMap_StructPointerInput(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - input := &testStruct{ - Name: "test-ptr", - Value: 456, - } - - result, err := toMap(input) - require.NoError(t, err) - require.Equal(t, "test-ptr", result["name"]) - require.Equal(t, float64(456), result["value"]) -} - -func TestToMap_NestedStructInput(t *testing.T) { - type inner struct { - InnerKey string `json:"innerKey"` - } - type outer struct { - OuterKey string `json:"outerKey"` - Nested inner `json:"nested"` - } - - input := outer{ - OuterKey: "outer-value", - Nested: inner{ - InnerKey: "inner-value", - }, - } - - result, err := toMap(input) - require.NoError(t, err) - require.Equal(t, "outer-value", result["outerKey"]) - - nested, ok := result["nested"].(map[string]any) - require.True(t, ok, "nested should be a map") - require.Equal(t, "inner-value", nested["innerKey"]) -} - -func TestToMap_MapStringInterfaceInput(t *testing.T) { - // map[string]interface{} is equivalent to map[string]any - input := map[string]interface{}{ - "key1": "value1", - "key2": 42, - } - - result, err := toMap(input) - require.NoError(t, err) - require.Equal(t, "value1", result["key1"]) - require.Equal(t, 42, result["key2"]) -} - -func TestToMap_NestedMapInput(t *testing.T) { - input := map[string]any{ - "level1": map[string]any{ - "level2": map[string]any{ - "deep": "value", - }, - }, - } - - result, err := toMap(input) - require.NoError(t, err) - - level1, ok := result["level1"].(map[string]any) - require.True(t, ok) - - level2, ok := level1["level2"].(map[string]any) - require.True(t, ok) - - require.Equal(t, "value", level2["deep"]) -} - -func TestToMap_WithNilValues(t *testing.T) { - input := map[string]any{ - "key1": "value1", - "nullKey": nil, - } - - result, err := toMap(input) - require.NoError(t, err) - require.Equal(t, "value1", result["key1"]) - require.Nil(t, result["nullKey"]) -} - -func TestToMap_WithArrayValues(t *testing.T) { - input := map[string]any{ - "tags": []string{"tag1", "tag2", "tag3"}, - } - - result, err := toMap(input) - require.NoError(t, err) - - tags, ok := result["tags"].([]string) - require.True(t, ok) - require.Len(t, tags, 3) - require.Equal(t, []string{"tag1", "tag2", "tag3"}, tags) -} - -func TestToMap_StructWithOmitEmpty(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Empty string `json:"empty,omitempty"` - Pointer *string `json:"pointer,omitempty"` - } - - input := testStruct{ - Name: "test", - Empty: "", - Pointer: nil, - } - - result, err := toMap(input) - require.NoError(t, err) - require.Equal(t, "test", result["name"]) - - // omitempty fields should not be present - _, hasEmpty := result["empty"] - require.False(t, hasEmpty, "empty field with omitempty should not be present") - - _, hasPointer := result["pointer"] - require.False(t, hasPointer, "nil pointer with omitempty should not be present") -} - -func TestToMap_StructWithAllTypes(t *testing.T) { - type testStruct struct { - String string `json:"string"` - Int int `json:"int"` - Float float64 `json:"float"` - Bool bool `json:"bool"` - Slice []string `json:"slice"` - Map map[string]int `json:"map"` - Pointer *string `json:"pointer"` - } - - strPtr := "pointer-value" - input := testStruct{ - String: "hello", - Int: 42, - Float: 3.14, - Bool: true, - Slice: []string{"a", "b"}, - Map: map[string]int{"x": 1, "y": 2}, - Pointer: &strPtr, - } - - result, err := toMap(input) - require.NoError(t, err) - - require.Equal(t, "hello", result["string"]) - require.Equal(t, float64(42), result["int"]) // JSON numbers are float64 - require.Equal(t, 3.14, result["float"]) - require.Equal(t, true, result["bool"]) - require.Equal(t, "pointer-value", result["pointer"]) - - slice, ok := result["slice"].([]any) - require.True(t, ok) - require.Len(t, slice, 2) - - mapVal, ok := result["map"].(map[string]any) - require.True(t, ok) - require.Equal(t, float64(1), mapVal["x"]) - require.Equal(t, float64(2), mapVal["y"]) -} - -func TestToMap_EmptyStruct(t *testing.T) { - type emptyStruct struct{} - - input := emptyStruct{} - - result, err := toMap(input) - require.NoError(t, err) - require.NotNil(t, result) - require.Empty(t, result) -} - -func TestToMap_AnonymousStruct(t *testing.T) { - input := struct { - Field1 string `json:"field1"` - Field2 int `json:"field2"` - }{ - Field1: "anonymous", - Field2: 999, - } - - result, err := toMap(input) - require.NoError(t, err) - require.Equal(t, "anonymous", result["field1"]) - require.Equal(t, float64(999), result["field2"]) -} - -func TestToMap_UnexportedFields(t *testing.T) { - type testStruct struct { - Exported string `json:"exported"` - unexported string //nolint:unused - } - - input := testStruct{ - Exported: "visible", - unexported: "hidden", - } - - result, err := toMap(input) - require.NoError(t, err) - require.Equal(t, "visible", result["exported"]) - - // unexported field should not be present - _, hasUnexported := result["unexported"] - require.False(t, hasUnexported, "unexported field should not be in result") -} - -func TestToMap_PrimitiveString(t *testing.T) { - // A primitive string cannot be converted to a map - input := "just a string" - - _, err := toMap(input) - require.Error(t, err, "primitive string should fail to convert to map") -} - -func TestToMap_PrimitiveInt(t *testing.T) { - // A primitive int cannot be converted to a map - input := 42 - - _, err := toMap(input) - require.Error(t, err, "primitive int should fail to convert to map") -} - -func TestToMap_SliceInput(t *testing.T) { - // A slice at the top level cannot be converted to a map - input := []string{"a", "b", "c"} - - _, err := toMap(input) - require.Error(t, err, "slice should fail to convert to map") -} diff --git a/apps/workspace-engine/pkg/workspace/jobs/helpers.go b/apps/workspace-engine/pkg/workspace/jobs/helpers.go deleted file mode 100644 index 8c8c51459..000000000 --- a/apps/workspace-engine/pkg/workspace/jobs/helpers.go +++ /dev/null @@ -1,45 +0,0 @@ -package jobs - -import ( - "encoding/json" - "maps" -) - -func toMap(v any) (map[string]any, error) { - if v == nil { - return map[string]any{}, nil - } - if m, ok := v.(map[string]any); ok { - return m, nil - } - if m, ok := v.(map[string]any); ok { - out := make(map[string]any, len(m)) - maps.Copy(out, m) - return out, nil - } - b, err := json.Marshal(v) - if err != nil { - return nil, err - } - var out map[string]any - if err := json.Unmarshal(b, &out); err != nil { - return nil, err - } - if out == nil { - out = map[string]any{} - } - return out, nil -} - -// deepMerge recursively merges src into dst. -func deepMerge(dst, src map[string]any) { - for k, v := range src { - if sm, ok := v.(map[string]any); ok { - if dm, ok := dst[k].(map[string]any); ok { - deepMerge(dm, sm) - continue - } - } - dst[k] = v // overwrite - } -} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/deployment/executor_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/deployment/executor_test.go index b950a0601..ec1632c17 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/deployment/executor_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/deployment/executor_test.go @@ -55,6 +55,30 @@ func createTestDeploymentForExecutor(id, systemID, name, jobAgentID string) *oap } } +func createTestEnvironmentForExecutor(id, systemID, name string) *oapi.Environment { + selector := &oapi.Selector{} + _ = selector.FromCelSelector(oapi.CelSelector{Cel: "true"}) + return &oapi.Environment{ + Id: id, + Name: name, + SystemId: systemID, + ResourceSelector: selector, + } +} + +func createTestResourceForExecutor(id, name, workspaceID string) *oapi.Resource { + return &oapi.Resource{ + Id: id, + Name: name, + Kind: "", + Identifier: name, + CreatedAt: time.Now(), + Config: map[string]any{}, + Metadata: map[string]string{}, + WorkspaceId: workspaceID, + } +} + func createTestRelease(deploymentID, environmentID, resourceID, versionID, versionTag string) *oapi.Release { return &oapi.Release{ ReleaseTarget: oapi.ReleaseTarget{ @@ -94,6 +118,12 @@ func TestExecuteRelease_Success(t *testing.T) { deployment := createTestDeploymentForExecutor(deploymentID, systemID, "test-deployment", jobAgentID) _ = testStore.Deployments.Upsert(ctx, deployment) + environment := createTestEnvironmentForExecutor(environmentID, systemID, "test-environment") + _ = testStore.Environments.Upsert(ctx, environment) + + resource := createTestResourceForExecutor(resourceID, "test-resource", workspaceID) + _, _ = testStore.Resources.Upsert(ctx, resource) + // Create release release := createTestRelease(deploymentID, environmentID, resourceID, versionID, "v1.0.0") diff --git a/apps/workspace-engine/pkg/workspace/workflowmanager/manager.go b/apps/workspace-engine/pkg/workspace/workflowmanager/manager.go index adddbf6fb..2106bcf85 100644 --- a/apps/workspace-engine/pkg/workspace/workflowmanager/manager.go +++ b/apps/workspace-engine/pkg/workspace/workflowmanager/manager.go @@ -128,21 +128,11 @@ func (m *Manager) CreateWorkflowRun(ctx context.Context, workflowId string, inpu } func (m *Manager) dispatchJob(ctx context.Context, wfJob *oapi.WorkflowJob) error { - jobAgent, ok := m.store.JobAgents.Get(wfJob.Ref) - if !ok { - return fmt.Errorf("job agent %s not found", wfJob.Ref) - } - - mergedConfig, err := mergeJobAgentConfig(jobAgent.Config, wfJob.Config) - if err != nil { - return fmt.Errorf("failed to merge job agent config: %w", err) - } - job := &oapi.Job{ Id: uuid.New().String(), WorkflowJobId: wfJob.Id, JobAgentId: wfJob.Ref, - JobAgentConfig: mergedConfig, + JobAgentConfig: oapi.JobAgentConfig{}, CreatedAt: time.Now(), UpdatedAt: time.Now(), Metadata: make(map[string]string), @@ -156,23 +146,3 @@ func (m *Manager) dispatchJob(ctx context.Context, wfJob *oapi.WorkflowJob) erro return nil } - -func mergeJobAgentConfig(configs ...oapi.JobAgentConfig) (oapi.JobAgentConfig, error) { - mergedConfig := make(map[string]any) - for _, config := range configs { - deepMerge(mergedConfig, config) - } - return mergedConfig, nil -} - -func deepMerge(dst, src map[string]any) { - for k, v := range src { - if sm, ok := v.(map[string]any); ok { - if dm, ok := dst[k].(map[string]any); ok { - deepMerge(dm, sm) - continue - } - } - dst[k] = v - } -}