feat: add stop lifecycle hook for external providers#13779
feat: add stop lifecycle hook for external providers#13779
Conversation
Provider-backed services were silently skipped on `docker compose stop`, leaving external resources running after the user expected the stack to be paused (e.g. after Ctrl+C during `up --watch`). Compose now invokes `<provider> compose stop <service>` for providers that advertise a `stop` block in their `metadata` subcommand output. Providers that do not advertise stop (or do not implement metadata at all) are silently skipped, preserving backward compatibility with existing providers that pre-date this hook. Closes #13772 Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds support for a provider “stop” lifecycle hook so provider-backed services are no longer skipped when users run docker compose stop (e.g., after Ctrl+C from up --watch). The hook is opt-in via provider compose metadata advertising a stop block, preserving backward compatibility.
Changes:
- Invoke provider
compose stop <service>duringdocker compose stopwhen the provider advertisesstopin metadata. - Extend provider metadata parsing to include an optional
stopcommand block and emit stop-related progress events. - Add unit + e2e coverage and update provider extension docs + example provider implementation.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/compose/stop.go | Adds stop-path logic to call provider stop hook when advertised by metadata. |
| pkg/compose/plugins.go | Adds stop handling to plugin execution flow and provider metadata model. |
| pkg/compose/plugins_test.go | Adds unit tests for ProviderMetadata.Stop parsing and IsEmpty() semantics. |
| pkg/e2e/providers_test.go | Adds e2e test asserting docker compose stop triggers provider stop hook. |
| pkg/e2e/fixtures/providers/provider-stop.yaml | New fixture to exercise provider-backed service stop behavior. |
| docs/extension.md | Documents the new stop lifecycle hook and metadata schema. |
| docs/examples/provider.go | Updates example provider to implement and advertise the stop hook. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if serv.Provider != nil { | ||
| path, err := s.getPluginBinaryPath(serv.Provider.Type) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| metadata := s.getPluginMetadata(path, serv.Provider.Type, project) | ||
| if metadata.Stop == nil { | ||
| return nil | ||
| } | ||
| return s.runPlugin(ctx, project, serv, "stop") |
| `docker compose start` or `docker compose up` can resume it. Any `setenv` JSON message returned during `stop` is ignored, | ||
| since dependent services are also stopping. |
The previous implementation fetched the provider binary path and metadata twice per service during `compose stop`: once in stop.go to gate on the `stop` capability, and again inside runPlugin via setupPluginCommand. setupPluginCommand now signals "skip" by returning (nil, nil) when the requested command is absent from the provider's metadata. stop.go calls runPlugin directly; the skip-when-unadvertised check moves into runPlugin. Addresses PR #13779 review feedback. Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
|
/review |
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
This PR adds a stop lifecycle hook for external provider plugins. The core design is solid — providers that do not advertise a stop block in their metadata are silently skipped (preserving backward compatibility), the runPlugin early-return after executing stop correctly avoids injecting environment variables that are only relevant to up, and the unit/e2e test coverage is appropriate.
One medium-severity issue was found in the fallback project-derivation path for provider-only stacks when options.Project is nil.
The previous implementation fetched the provider binary path and metadata twice per service during `compose stop`: once in stop.go to gate on the `stop` capability, and again inside runPlugin via setupPluginCommand. setupPluginCommand now signals "skip" by returning (nil, nil) when the requested command is absent from the provider's metadata. stop.go calls runPlugin directly; the skip-when-unadvertised check moves into runPlugin. Addresses PR #13779 review feedback. Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
a9cc6bb to
38a12cf
Compare
|
/review |
A previous change extended IsEmpty() to include `Stop == nil`. For a provider that advertises only a `stop` block, this caused commandMetadataIsEmpty to be false in setupPluginCommand, which made the option-forwarding filter reject every key — silently dropping all provider options on `up` and `down`. The Stop-presence signal lives independently in stop.go and the "stop" case of setupPluginCommand, so reverting the IsEmpty check to its original semantics is sufficient. Addresses PR #13779 review feedback. Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
|
/review |
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
Reviewed feat: add stop lifecycle hook for external providers (PR #13779).
Summary: The implementation is clean and correct. The stop lifecycle hook is well-integrated into the existing provider plugin infrastructure with proper backward compatibility.
What was reviewed:
pkg/compose/stop.go: Provider-backed services are now correctly routed torunPlugin(..., "stop")instead ofstopContainers. The guard is correctly placed before the container stop path.pkg/compose/plugins.go: ThesetupPluginCommandreturns(nil, nil)when a provider's metadata has nostopblock, andrunPluginguards against a nilcmdwith an early return — clean opt-in design preserving backward compatibility.executePlugin: Correctly firesstoppingEvent/stoppedEventfor the stop case, and early-returns before thesetenvvariable propagation (which is intentionally skipped for stop, as documented).plugins_test.go: Good coverage ofIsEmpty()edge cases and JSON unmarshaling of the newStopfield, including the{"stop":{"parameters":null}}corner case.providers_test.go:TestProviderStopHookuses a sentinel file to verify the provider binary's stop subcommand is invoked end-to-end.docs/examples/provider.go: Example provider correctly advertisesstopin metadata and usesPROVIDER_STOP_MARKERenv var for test verification.docs/extension.md: Documentation accurately describes the opt-in nature, the ignoredsetenvmessages during stop, and the--timeoutflag scope limitation.
No bugs, race conditions, or error-handling issues found.
What I did
Provider-backed services were silently skipped on
docker compose stop, leaving external resources running after the user expected the stack to be paused (e.g. after Ctrl+C duringup --watch).Compose now invokes
<provider> compose stop <service>for providers that advertise astopblock in theirmetadatasubcommand output. Providers that do not advertise stop (or do not implement metadata at all) are silently skipped, preserving backward compatibility with existing providers that pre-date this hook.Related issue
Closes #13772
(not mandatory) A picture of a cute animal, if possible in relation to what you did
