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 {