feat: add stop lifecycle hook for external providers

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>
This commit is contained in:
Guillaume Lours 2026-05-07 18:38:41 +02:00 committed by Guillaume Lours
parent 8e0d5e17a7
commit 672dc14d29
7 changed files with 235 additions and 14 deletions

View file

@ -74,8 +74,14 @@ func composeCommand() *cobra.Command {
downCmd.Flags().String("name", "", "Name of the database to be deleted")
_ = downCmd.MarkFlagRequired("name")
c.AddCommand(upCmd, downCmd)
c.AddCommand(metadataCommand(upCmd, downCmd))
stopCmd := &cobra.Command{
Use: "stop",
Run: stop,
Args: cobra.ExactArgs(1),
}
c.AddCommand(upCmd, downCmd, stopCmd)
c.AddCommand(metadataCommand(upCmd, downCmd, stopCmd))
return c
}
@ -96,21 +102,29 @@ func down(_ *cobra.Command, _ []string) {
fmt.Printf(`{ "type": "error", "message": "Permission error" }%s`, lineSeparator)
}
func metadataCommand(upCmd, downCmd *cobra.Command) *cobra.Command {
func stop(_ *cobra.Command, _ []string) {
if marker := os.Getenv("PROVIDER_STOP_MARKER"); marker != "" {
_ = os.WriteFile(marker, []byte("stopped"), 0o600)
}
}
func metadataCommand(upCmd, downCmd, stopCmd *cobra.Command) *cobra.Command {
return &cobra.Command{
Use: "metadata",
Run: func(cmd *cobra.Command, _ []string) {
metadata(upCmd, downCmd)
metadata(upCmd, downCmd, stopCmd)
},
Args: cobra.NoArgs,
}
}
func metadata(upCmd, downCmd *cobra.Command) {
func metadata(upCmd, downCmd, stopCmd *cobra.Command) {
metadata := ProviderMetadata{}
metadata.Description = "Manage services on AwesomeCloud"
metadata.Up = commandParameters(upCmd)
metadata.Down = commandParameters(downCmd)
stopParams := commandParameters(stopCmd)
metadata.Stop = &stopParams
jsonMetadata, err := json.Marshal(metadata)
if err != nil {
panic(err)
@ -134,9 +148,10 @@ func commandParameters(cmd *cobra.Command) CommandMetadata {
}
type ProviderMetadata struct {
Description string `json:"description"`
Up CommandMetadata `json:"up"`
Down CommandMetadata `json:"down"`
Description string `json:"description"`
Up CommandMetadata `json:"up"`
Down CommandMetadata `json:"down"`
Stop *CommandMetadata `json:"stop,omitempty"`
}
type CommandMetadata struct {

View file

@ -30,7 +30,7 @@ the resource(s) needed to run a service.
If `provider.type` doesn't resolve into any of those, Compose will report an error and interrupt the `up` command.
To be a valid Compose extension, provider command *MUST* accept a `compose` command (which can be hidden)
with subcommands `up` and `down`.
with subcommands `up` and `down`. It *MAY* additionally implement a `stop` subcommand to support `docker compose stop`.
## Up lifecycle
@ -107,6 +107,20 @@ into its runtime environment.
`down` lifecycle is equivalent to `up` with the `<provider> compose --project-name <NAME> down <SERVICE>` command.
The provider is responsible for releasing all resources associated with the service.
## Stop lifecycle
When the user runs `docker compose stop`, Compose invokes `<provider> compose --project-name <NAME> stop <SERVICE>` for each
provider-backed service in reverse dependency order. The provider should pause the resource without releasing it, so a later
`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 `stop` hook is opt-in: Compose invokes it only when the provider declares a `stop` block in its `metadata` subcommand
output. Providers that do not advertise `stop` in metadata (or do not implement the `metadata` subcommand at all) are
silently skipped during `docker compose stop`, preserving backward compatibility with providers that pre-date this hook.
The `--timeout` flag of `docker compose stop` applies only to container services; provider stop hooks are not subject to
this timeout and are responsible for managing their own shutdown duration.
## Provide metadata about options
Compose extensions *MAY* optionally implement a `metadata` subcommand to provide information about the parameters accepted by the `up` and `down` commands.
@ -153,6 +167,16 @@ The expected JSON output format is:
"type": "string"
}
]
},
"stop": {
"parameters": [
{
"name": "name",
"description": "Name of the database to be stopped",
"required": true,
"type": "string"
}
]
}
}
```
@ -160,6 +184,7 @@ The top elements are:
- `description`: Human-readable description of the provider
- `up`: Object describing the parameters accepted by the `up` command
- `down`: Object describing the parameters accepted by the `down` command
- `stop`: Object describing the parameters accepted by the `stop` command (optional)
And for each command parameter, you should include the following properties:
- `name`: The parameter name (without `--` prefix)