mirror of
https://github.com/docker/compose.git
synced 2026-10-12 02:13:08 +00:00
run: honor the explicit manual: false opt-out on jobs
Per the spec, any job can be triggered manually by an explicit run command, its automated triggers notwithstanding — so run now accepts scheduled jobs too. The exception is a job explicitly declared with triggers.manual: false, which run rejects: meant for scheduled jobs whose out-of-schedule execution would be harmful. compose-go is bumped to the jobs-branch commit making Manual tri-state (*bool) and allowing manual and schedule to be combined. Signed-off-by: Nicolas De Loof <[email protected]>
This commit is contained in:
committed by
Nicolas De loof
parent
135fb0ed3a
commit
2804f9d4ae
+8
-6
@@ -403,10 +403,12 @@ func runRun(ctx context.Context, backend api.Compose, project *types.Project, op
|
||||
return err
|
||||
}
|
||||
|
||||
// materializeManualJob lets run target a manual-trigger job exactly like a
|
||||
// service. A job is a ContainerSpec+WorkloadSpec — the same layers a service
|
||||
// is made of — so it materializes as a service for the one-off machinery:
|
||||
// its profile is activated and the project narrowed to its dependencies by
|
||||
// materializeManualJob lets run target a job exactly like a service: per the
|
||||
// spec, any job can be triggered manually regardless of its automated
|
||||
// triggers, unless it explicitly opts out with `triggers.manual: false`.
|
||||
// A job is a ContainerSpec+WorkloadSpec — the same layers a service is made
|
||||
// of — so it materializes as a service for the one-off machinery: its
|
||||
// profile is activated and the project narrowed to its dependencies by
|
||||
// WithSelectedJob, then the job joins Services under its own name.
|
||||
func materializeManualJob(project *types.Project, name string) (*types.Project, error) {
|
||||
// jobs and services share the depends_on namespace but not their own: a
|
||||
@@ -420,8 +422,8 @@ func materializeManualJob(project *types.Project, name string) (*types.Project,
|
||||
if !ok {
|
||||
return project, nil
|
||||
}
|
||||
if job.Triggers == nil || job.Triggers.Manual == nil || !*job.Triggers.Manual {
|
||||
return nil, fmt.Errorf("job %q has no manual trigger, it cannot be run", name)
|
||||
if job.Triggers != nil && job.Triggers.Manual != nil && !*job.Triggers.Manual {
|
||||
return nil, fmt.Errorf("job %q is declared with manual: false, it cannot be run manually", name)
|
||||
}
|
||||
project, err := project.WithSelectedJob(name)
|
||||
if err != nil {
|
||||
|
||||
+19
-6
@@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMaterializeManualJob(t *testing.T) {
|
||||
yes := true
|
||||
yes, no := true, false
|
||||
base := func() *types.Project {
|
||||
return &types.Project{
|
||||
Services: types.Services{
|
||||
@@ -39,8 +39,13 @@ func TestMaterializeManualJob(t *testing.T) {
|
||||
WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{"db": {Condition: types.ServiceConditionStarted, Required: true}}},
|
||||
},
|
||||
"backup": {
|
||||
Name: "backup",
|
||||
Triggers: &types.TriggerConfig{Schedule: []types.ScheduleConfig{{Cron: "0 3 * * *"}}},
|
||||
Name: "backup",
|
||||
Triggers: &types.TriggerConfig{Schedule: []types.ScheduleConfig{{Cron: "0 3 * * *"}}},
|
||||
ContainerSpec: types.ContainerSpec{Image: "backup-tool"},
|
||||
},
|
||||
"rotation": {
|
||||
Name: "rotation",
|
||||
Triggers: &types.TriggerConfig{Manual: &no, Schedule: []types.ScheduleConfig{{Cron: "0 3 1 * *"}}},
|
||||
},
|
||||
"prep": {
|
||||
Name: "prep",
|
||||
@@ -105,9 +110,17 @@ func TestMaterializeManualJob(t *testing.T) {
|
||||
assert.Equal(t, svc.Image, "the-service", "a same-named service must not be shadowed by the job")
|
||||
})
|
||||
|
||||
t.Run("a schedule-only job is rejected", func(t *testing.T) {
|
||||
_, err := materializeManualJob(base(), "backup")
|
||||
assert.Error(t, err, `job "backup" has no manual trigger, it cannot be run`)
|
||||
t.Run("a scheduled job without explicit manual opt-out can be run", func(t *testing.T) {
|
||||
got, err := materializeManualJob(base(), "backup")
|
||||
assert.NilError(t, err)
|
||||
svc, err := got.GetService("backup")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, svc.Image, "backup-tool")
|
||||
})
|
||||
|
||||
t.Run("manual: false explicitly forbids manual execution", func(t *testing.T) {
|
||||
_, err := materializeManualJob(base(), "rotation")
|
||||
assert.Error(t, err, `job "rotation" is declared with manual: false, it cannot be run manually`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+11
-7
@@ -32,18 +32,22 @@ func TestUpRejectsScheduledJobs(t *testing.T) {
|
||||
ServiceNotCreated("web"))
|
||||
}
|
||||
|
||||
// A manual-trigger job runs through `compose run` exactly like a service
|
||||
// would: its declared dependencies start first, its output and exit flow
|
||||
// back. A schedule-only job stays out of reach.
|
||||
// A job runs through `compose run` exactly like a service would: its
|
||||
// declared dependencies start first, its output and exit flow back. Per the
|
||||
// spec, manual execution is always available — scheduled jobs included —
|
||||
// unless the job explicitly opts out with `triggers.manual: false`.
|
||||
func TestRunManualJob(t *testing.T) {
|
||||
NewScenario(t, "run must execute a manual job like a service, starting its depends_on services first").
|
||||
NewScenario(t, "run must execute a job like a service, starting its depends_on services first").
|
||||
Step("run executes the job after starting its dependency",
|
||||
ComposeCmd("run", "--rm", "migrate"),
|
||||
OutputContains("migration done"),
|
||||
ServiceState("db", "running")).
|
||||
Step("a schedule-only job cannot be run",
|
||||
ComposeCmd("run", "--rm", "backup").MayFail(),
|
||||
StderrContains(`job "backup" has no manual trigger`))
|
||||
Step("a scheduled job without explicit opt-out can be run manually",
|
||||
ComposeCmd("run", "--rm", "backup"),
|
||||
OutputContains("backup")).
|
||||
Step("manual: false explicitly forbids manual execution",
|
||||
ComposeCmd("run", "--rm", "rotation").MayFail(),
|
||||
StderrContains(`job "rotation" is declared with manual: false`))
|
||||
}
|
||||
|
||||
// A job's own env_file resolves exactly like a selected service's would: the
|
||||
|
||||
@@ -18,3 +18,10 @@ jobs:
|
||||
triggers:
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
rotation:
|
||||
image: alpine
|
||||
command: echo rotation
|
||||
triggers:
|
||||
manual: false
|
||||
schedule:
|
||||
- cron: "0 3 1 * *"
|
||||
|
||||
Reference in New Issue
Block a user