Conversation
f6ea219 to
ccaa8ce
Compare
ccaa8ce to
31714e5
Compare
8114744 to
3811fa7
Compare
compose-spec/compose-go#866 is merged: jobs top-level element, container specification layered as ContainerSpec/WorkloadSpec, pre_start hooks carrying the full container specification resolved at load time. Bump to the merged head and adapt in the same movement — composite literals setting moved fields wrap them into the embedded ContainerSpec / WorkloadSpec (promoted field access was already source-compatible, so this is literal-only, no behavior change), and pre_start handling is typed against PreStartHook. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
… start The model may declare jobs this runtime cannot execute yet. Commands that materialize the application warn about them, and refuse an active (profile-enabled) scheduled job before any resource is created — up, create and start alike: silently not scheduling would break expectations, while manual jobs just wait for an explicit trigger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
`docker compose run <job>` materializes the job — a ContainerSpec + WorkloadSpec, the same layers a service is made of — as a service for the one-off machinery: its profile is activated, the project narrowed to its dependencies by WithSelectedJob, and the exit code flows back. The materialization happens during project loading, BEFORE service selection and environment resolution: the job's own env_file and label_file resolve exactly like a selected service's would, unrelated services' env_file still don't need to exist, and a target that is neither a service nor a job keeps the precise selection error (the retry is keyed on "the target is a declared job", never on matching an error message). A service and a job may share a name: the service wins, matching what the selector resolved. A job may depend on other jobs: the whole job closure materializes, so every depends_on edge resolves to a runnable service satisfying its declared condition — a dependency job runs to completion through the exact machinery a service dependency does — instead of dangling as an unresolvable name. Job extensions (x-*) survive materialization. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
pre_start hooks carry the full container specification, but the init-container runner only consumed a handful of attributes (image, command, user, env, workdir) — everything else was silently dropped. Instead of wiring attributes one by one, the hook's specification is merged over the service's ContainerSpec through compose-go's own file merge machinery (override.Merge on the canonical yaml tree): command and entrypoint replace, environment merges per key with the hook winning, extra_hosts and dns accumulate entries, ulimits merge — the exact per-attribute rules of multi-file compose, maintained in one place. Every ContainerSpec attribute inherits this way, current and future, with zero attribute-specific code. The merged spec then runs through the standard create path (getCreateConfigs) as a service-shaped one-off, so resources, capabilities, dns, sysctls, logging... materialize exactly as they would for a service container. Hook containers keep their minimal labels and carry no container-number, so tooling telling replicas apart does not count them. The only deliberate exception is volumes: mounts inherit at runtime through volumes_from — the only mechanism that shares the service's anonymous and image volumes — and the hook's own volume declarations, materialized by the standard path, take precedence per target. This is what lets an init container get read-write access to a volume the service mounts read-only (fixes: see PR). e2e scenarios lock extra_hosts inheritance and accumulation, volume override and completion, and the unit tests pin the merge rules. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
…chain Scenario-DSL coverage for the jobs entry points: - up refuses a project declaring an active scheduled job before creating any resource; - run executes a manual job like a service, starting its depends_on services first, and refuses a schedule-only job; - a job's own env_file feeds its environment through run — the materialization happens before environment resolution; - a job depending on another job runs the dependency to completion first, through the exact machinery a service dependency uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
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 <nicolas.deloof@gmail.com>
compose-go resolves each hook against its service at load time: the model itself carries the full container specification a hook runs with, and the runtime consumes it as-is through the standard create path — the runtime-side merge helper goes away. Consuming the full spec means honoring ALL of it: - service references it may carry — volumes_from entries, and service:-scoped network_mode/ipc/pid, inherited or declared — resolve to live container IDs exactly like the service create path does; the daemon knows nothing about service names and rejected them, failing up for any service combining volumes_from with a pre_start hook (locked by an e2e scenario). - hook labels — declared or inherited — merge into the container's labels, the runtime identification set winning on conflicts. - the <API 1.44 network-connect fallback joins the HOOK's networks, not the parent service's: a hook overriding networks was connected to the wrong ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Every publish safeguard and image-pinning path used to walk only project.Services, leaving jobs invisible: - the sensitive-data checks (literal environment values, env_file scans, bind-mount warnings, build-only rejection) now cover jobs — a job declaring AWS_SECRET_ACCESS_KEY=... was published without the guard rail a service gets; - the image-digest override pins job images too (jobs dressed as services run through the exact WithImagesResolved semantics), so the published artifact is reproducible for jobs as well; - the application index references job images; - `config --images` lists them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
3811fa7 to
0a8ee1b
Compare
glours
left a comment
There was a problem hiding this comment.
A few things worth fixing before this lands, even given the interim/client-side nature of this jobs support:
cmd/compose/up.go:362 — warnIgnoredJobs iterates project.AllJobs() (includes profile-disabled jobs), while rejectScheduledJobs (up.go:379, also called from create.go:104 and start.go:63) correctly uses project.Jobs (profile-enabled
only). Result: up can warn about jobs that aren't even part of this invocation's profile selection. Should use project.Jobs here too.
cmd/compose/run.go:287-292 — the job-detection fallback now retries the full project load on any narrowed-load error, not just "no such service". Any unrelated load failure (bad include:, interpolation error) now triggers a second full
load — duplicating remote include: fetches and unsupported-attribute warnings — before falling through to the (still correct) original error. Worth narrowing the retry condition back to the selection-failure case, since job detection doesn't
need the broader retry.
pkg/compose/pre_start_test.go — still no test exercising the security-sensitive fields (Privileged, CapAdd/CapDrop, SecurityOpt, ReadOnly, Sysctls) that hooks now inherit from the service by default. This is pre-existing/permanent
hook behavior, not something the interim jobs work will replace — worth covering given what's at stake if inheritance breaks silently.
pkg/compose/publish.go — the new job branches (pushApplicationIndex, generateImageDigestsOverride, collectEnvCheckFindings, checkOnlyBuildSection, checkForBindMount, checkForSensitiveData) are correct on inspection but have zero
dedicated test coverage — publish_test.go's only changes are mechanical ContainerSpec adaptations. This is the secret-scanning gate for publish; a future regression here would go undetected by CI.
Nits, non-blocking:
- pre_start.go:
resolveHookServiceReferencesrebuilds the per-service container map inline instead of reusinggetContainersByService. - publish.go: findings for a job are still labeled
"service %q: ..."in the merged prompt message.
Adopts compose-spec/compose-go#866 (jobs top-level element, container specification layered as ContainerSpec/WorkloadSpec, pre_start hooks carrying the full container specification).
This first step adapts the types and gives jobs their first runtime entry point:
composite literals setting moved fields are wrapped into the embedded
ContainerSpec/WorkloadSpec(no behavior change; promoted field access was already source-compatible)pre_starthandling is typed againstPreStartHook; exec hooks (post_start/pre_stop) lose the image field they never honored — now enforced by constructionupwarns about declared jobs (pointing atdocker compose runto trigger them) and fails on active (profile-enabled) scheduled jobs withscheduled jobs are not supported in this version: silently not scheduling would break expectations, while manual jobs just wait for an explicit triggerdocker compose run <job>executes a job exactly like a service: the job — aContainerSpec+WorkloadSpec, the same layers a service is made of — is materialized as a service for the one-off machinery; its profile is activated, its dependencies start as they would for a service, the exit code flows back. Per the spec, manual execution is always available — scheduled jobs included — unless the job explicitly opts out withtriggers.manual: false(tri-state*boolin compose-go), which run rejects. All locked by e2e scenarios (scheduled rejection byupbefore any resource is created; manual run starting itsdepends_onservices first;manual: falserefusal).pre_starthooks inherit the whole container specification, resolved at load time: compose-go completes each hook with the service's container-spec attributes while loading (idempotently acrossconfiground-trips, thanks to the strictly-identical-entry merge dedup), so the model itself carries the spec a hook runs with and the runtime consumes it as-is through the standard create path — everyContainerSpecattribute (resources, capabilities, dns, sysctls, ...) inherits with zero attribute-specific code, following the exact multi-file merge rules (command replaces, environment merges per key, extra_hosts accumulate). The only deliberate exception is volumes, inherited at runtime throughvolumes_from(the only way to share anonymous and image volumes), with hook-declared mounts taking precedence per target. This closes the two standing asks against init containers: inheriting/declaringextra_hosts(fixes pre_start containers don't inherit/support extra_hosts #13939) and giving the hook read-write access to a volume the service mounts read-only (fixes pre_start/init-Containers should support own/overwritten Volumes #13934) — both locked by e2e scenarios.compose-spec/compose-go#866 is merged:
go.modnow points at the merged compose-go head (v2.15.1-0.20260910154416-11feead015fc), no replace directive involved — this PR is self-contained and mergeable. Jobs are hereby exposed end to end: declared in the model, warned about and guarded byup(scheduled jobs rejected until a scheduler exists), and runnable throughdocker compose run <job>. A compose-go tagged release can still substitute the pseudo-version before merge if preferred.🤖 Generated with Claude Code
Update (independent review follow-ups). An adversarial review pass surfaced real gaps, all addressed in the rebuilt stack:
volumes_from(andservice:-scopednetwork_mode/ipc/pid) inherited into apre_starthook now resolve to live container IDs like the service create path — the daemon used to receive raw service names and failupfor any service combiningvolumes_fromwith a hook (e2e-locked). Hooklabelsmerge into the container's labels (the runtime identification set wins), and the <API 1.44 network fallback joins the hook's own networks.env_file/label_fileresolve throughrun: materialization now happens during loading, before environment resolution — and the retry is keyed on "the target is a declared job", never on matching an error message, so a plain typo keeps the precise selection error and unrelated services' env_file still don't need to exist.depends_onexecutes: the whole job closure materializes, each dependency running to completion through the machinery a service dependency uses (e2e-locked). Materialized jobs carry the standard compose labels — without them their containers were invisible to every label-driven path — and theirx-*extensions.createandstartrefuse active scheduled jobs likeupdoes, keeping the "before any resource is created" promise on every entry point.publishsafeguards and image pinning cover jobs: sensitive-env/env_file/bind-mount/build-only checks, digest override, application index, andconfig --images.Note on config hashes: the container-spec layering reorders
ServiceConfigfields, which would have changed every service's config-hash (mass recreation on upgrade) — #14215 pins the hash byte layout to its historical form — fully backward compatible, zero recreation — and must land first; this PR then deletes its transient continuity test in the compose-go bump commit, as that test documents.