From 3882e5e606574a3435719787a9a65205096f14e3 Mon Sep 17 00:00:00 2001 From: Martijn Vermeulen Date: Thu, 5 Nov 2026 16:43:46 +0100 Subject: [PATCH] Apply pause presets to job triggers Co-authored-by: Isaac --- .../bundles/triggers-pause-presets.md | 1 + .../databricks.yml | 24 ++++++ .../out.test.toml | 2 + .../presets_triggers_pause_status/output.txt | 52 +++++++++++ .../presets_triggers_pause_status/script | 1 + .../mutator/resourcemutator/apply_presets.go | 5 ++ .../resourcemutator/apply_presets_test.go | 86 +++++++++++++++++++ .../resourcemutator/apply_target_mode_test.go | 37 ++++++++ 8 files changed, 208 insertions(+) create mode 100644 .nextchanges/bundles/triggers-pause-presets.md create mode 100644 acceptance/bundle/validate/presets_triggers_pause_status/databricks.yml create mode 100644 acceptance/bundle/validate/presets_triggers_pause_status/out.test.toml create mode 100644 acceptance/bundle/validate/presets_triggers_pause_status/output.txt create mode 100644 acceptance/bundle/validate/presets_triggers_pause_status/script diff --git a/.nextchanges/bundles/triggers-pause-presets.md b/.nextchanges/bundles/triggers-pause-presets.md new file mode 100644 index 00000000000..4b0974a602d --- /dev/null +++ b/.nextchanges/bundles/triggers-pause-presets.md @@ -0,0 +1 @@ +* Apply `presets.trigger_pause_status` and the development mode pause default to `triggers[]` entries on jobs. diff --git a/acceptance/bundle/validate/presets_triggers_pause_status/databricks.yml b/acceptance/bundle/validate/presets_triggers_pause_status/databricks.yml new file mode 100644 index 00000000000..577d7982bea --- /dev/null +++ b/acceptance/bundle/validate/presets_triggers_pause_status/databricks.yml @@ -0,0 +1,24 @@ +bundle: + name: test-bundle + +resources: + jobs: + job1: + name: job1 + triggers: + - schedule: + quartz_cron_expression: "0 0 0 1 1 ? 2099" + timezone_id: UTC + - schedule: + quartz_cron_expression: "0 0 0 2 1 ? 2099" + timezone_id: UTC + pause_status: UNPAUSED + + job2: + name: job2 + triggers: + - file_arrival: + url: /Volumes/main/default/landing/ + +presets: + trigger_pause_status: PAUSED diff --git a/acceptance/bundle/validate/presets_triggers_pause_status/out.test.toml b/acceptance/bundle/validate/presets_triggers_pause_status/out.test.toml new file mode 100644 index 00000000000..a927a5fbc06 --- /dev/null +++ b/acceptance/bundle/validate/presets_triggers_pause_status/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/validate/presets_triggers_pause_status/output.txt b/acceptance/bundle/validate/presets_triggers_pause_status/output.txt new file mode 100644 index 00000000000..0d08d96c3eb --- /dev/null +++ b/acceptance/bundle/validate/presets_triggers_pause_status/output.txt @@ -0,0 +1,52 @@ +{ + "job1": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "job1", + "queue": { + "enabled": true + }, + "triggers": [ + { + "pause_status": "PAUSED", + "schedule": { + "quartz_cron_expression": "0 0 0 1 1 ? 2099", + "timezone_id": "UTC" + } + }, + { + "pause_status": "UNPAUSED", + "schedule": { + "quartz_cron_expression": "0 0 0 2 1 ? 2099", + "timezone_id": "UTC" + } + } + ] + }, + "job2": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "job2", + "queue": { + "enabled": true + }, + "triggers": [ + { + "file_arrival": { + "url": "/Volumes/main/default/landing/" + }, + "pause_status": "PAUSED" + } + ] + } +} diff --git a/acceptance/bundle/validate/presets_triggers_pause_status/script b/acceptance/bundle/validate/presets_triggers_pause_status/script new file mode 100644 index 00000000000..2a5a539f62e --- /dev/null +++ b/acceptance/bundle/validate/presets_triggers_pause_status/script @@ -0,0 +1 @@ +$CLI bundle validate -o json | jq .resources.jobs diff --git a/bundle/config/mutator/resourcemutator/apply_presets.go b/bundle/config/mutator/resourcemutator/apply_presets.go index 9f2faab8398..8db0173a877 100644 --- a/bundle/config/mutator/resourcemutator/apply_presets.go +++ b/bundle/config/mutator/resourcemutator/apply_presets.go @@ -90,6 +90,11 @@ func (m *applyPresets) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnos if j.Trigger != nil && j.Trigger.PauseStatus == "" { j.Trigger.PauseStatus = paused } + for i := range j.Triggers { + if j.Triggers[i].PauseStatus == "" { + j.Triggers[i].PauseStatus = paused + } + } } } diff --git a/bundle/config/mutator/resourcemutator/apply_presets_test.go b/bundle/config/mutator/resourcemutator/apply_presets_test.go index c97db137b26..5fb868ad2b2 100644 --- a/bundle/config/mutator/resourcemutator/apply_presets_test.go +++ b/bundle/config/mutator/resourcemutator/apply_presets_test.go @@ -7,6 +7,7 @@ import ( "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/databricks-sdk-go/service/catalog" + "github.com/databricks/databricks-sdk-go/service/jobs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -38,3 +39,88 @@ func TestApplyPresetsSkipsNilQualityMonitors(t *testing.T) { require.NoError(t, diags.Error()) assert.Nil(t, b.Config.Resources.QualityMonitors["monitor"].Schedule) } + +func triggerWithPauseStatus(c jobs.TriggerConfiguration, s jobs.PauseStatus) jobs.TriggerConfiguration { + c.PauseStatus = s + return c +} + +func TestApplyPresetsTriggersPauseStatus(t *testing.T) { + cron := jobs.TriggerConfiguration{ + Schedule: &jobs.CronTriggerConfiguration{QuartzCronExpression: "0 0 0 1 1 ? 2099", TimezoneId: "UTC"}, + } + fileArrival := jobs.TriggerConfiguration{ + FileArrival: &jobs.FileArrivalTriggerConfiguration{Url: "/Volumes/main/default/landing/"}, + } + tableUpdate := jobs.TriggerConfiguration{ + TableUpdate: &jobs.TableUpdateTriggerConfiguration{TableNames: []string{"main.default.src"}}, + } + type testCase struct { + preset string + triggers []jobs.TriggerConfiguration + expected []jobs.PauseStatus + } + + tests := map[string]testCase{ + "paused preset, single omitted": { + preset: config.Paused, + triggers: []jobs.TriggerConfiguration{cron}, + expected: []jobs.PauseStatus{jobs.PauseStatusPaused}, + }, + "paused preset, multiple omitted": { + preset: config.Paused, + triggers: []jobs.TriggerConfiguration{cron, fileArrival, tableUpdate}, + expected: []jobs.PauseStatus{jobs.PauseStatusPaused, jobs.PauseStatusPaused, jobs.PauseStatusPaused}, + }, + "unpaused preset, multiple omitted": { + preset: config.Unpaused, + triggers: []jobs.TriggerConfiguration{cron, fileArrival}, + expected: []jobs.PauseStatus{jobs.PauseStatusUnpaused, jobs.PauseStatusUnpaused}, + }, + "explicit values preserved": { + preset: config.Paused, + triggers: []jobs.TriggerConfiguration{ + triggerWithPauseStatus(cron, jobs.PauseStatusUnpaused), + triggerWithPauseStatus(fileArrival, jobs.PauseStatusPaused), + }, + expected: []jobs.PauseStatus{jobs.PauseStatusUnpaused, jobs.PauseStatusPaused}, + }, + "mixed explicit and omitted": { + preset: config.Unpaused, + triggers: []jobs.TriggerConfiguration{ + triggerWithPauseStatus(cron, jobs.PauseStatusPaused), + fileArrival, + tableUpdate, + }, + expected: []jobs.PauseStatus{jobs.PauseStatusPaused, jobs.PauseStatusUnpaused, jobs.PauseStatusUnpaused}, + }, + "no preset leaves omitted values unset": { + triggers: []jobs.TriggerConfiguration{cron, triggerWithPauseStatus(fileArrival, jobs.PauseStatusPaused)}, + expected: []jobs.PauseStatus{"", jobs.PauseStatusPaused}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Presets: config.Presets{TriggerPauseStatus: tc.preset}, + Resources: config.Resources{ + Jobs: map[string]*resources.Job{ + "job": {JobSettings: jobs.JobSettings{Name: "job", Triggers: tc.triggers}}, + }, + }, + }, + } + + diags := bundle.Apply(t.Context(), b, ApplyPresets()) + require.NoError(t, diags.Error()) + + var actual []jobs.PauseStatus + for _, tr := range b.Config.Resources.Jobs["job"].Triggers { + actual = append(actual, tr.PauseStatus) + } + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/bundle/config/mutator/resourcemutator/apply_target_mode_test.go b/bundle/config/mutator/resourcemutator/apply_target_mode_test.go index 2e3320efbaa..ceb74bd9098 100644 --- a/bundle/config/mutator/resourcemutator/apply_target_mode_test.go +++ b/bundle/config/mutator/resourcemutator/apply_target_mode_test.go @@ -461,6 +461,43 @@ func TestProcessTargetModeDevelopment(t *testing.T) { assert.Empty(t, b.Config.Resources.Alerts["alert3"].Schedule.PauseStatus) } +func TestProcessTargetModeDevelopmentTriggersPauseStatus(t *testing.T) { + type testCase struct { + preset string + expected jobs.PauseStatus + } + + tests := map[string]testCase{ + "defaults to paused": {expected: jobs.PauseStatusPaused}, + "unpaused preset is honoured": {preset: config.Unpaused, expected: jobs.PauseStatusUnpaused}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + b := mockBundle(config.Development) + b.Config.Presets.TriggerPauseStatus = tc.preset + b.Config.Resources.Jobs["multi_triggers"] = &resources.Job{ + Name: "multi_triggers", + Triggers: []jobs.TriggerConfiguration{ + {Schedule: &jobs.CronTriggerConfiguration{QuartzCronExpression: "0 0 0 1 1 ? 2099", TimezoneId: "UTC"}}, + {FileArrival: &jobs.FileArrivalTriggerConfiguration{Url: "/Volumes/main/default/landing/"}}, + { + Schedule: &jobs.CronTriggerConfiguration{QuartzCronExpression: "0 0 0 2 1 ? 2099", TimezoneId: "UTC"}, + PauseStatus: jobs.PauseStatusPaused, + }, + }, + } + + diags := bundle.ApplySeq(t.Context(), b, ApplyTargetMode(), ApplyPresets()) + require.NoError(t, diags.Error()) + + triggers := b.Config.Resources.Jobs["multi_triggers"].Triggers + actual := []jobs.PauseStatus{triggers[0].PauseStatus, triggers[1].PauseStatus, triggers[2].PauseStatus} + assert.Equal(t, []jobs.PauseStatus{tc.expected, tc.expected, jobs.PauseStatusPaused}, actual) + }) + } +} + func TestProcessTargetModeDevelopmentTagNormalizationForAws(t *testing.T) { b := mockBundle(config.Development) b.Tagging = tags.ForCloud(&sdkconfig.Config{