mirror of
https://github.com/docker/compose.git
synced 2026-05-13 13:58:02 +00:00
Merge cb08950fda into 659b269e52
This commit is contained in:
commit
fe2bab7bec
7 changed files with 225 additions and 13 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 up` can resume it (note that `docker compose start` only restarts existing containers and does not invoke
|
||||
provider hooks). 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)
|
||||
|
|
|
|||
|
|
@ -66,12 +66,19 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project,
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
variables, err := s.executePlugin(cmd, command, service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if command == "stop" {
|
||||
return nil
|
||||
}
|
||||
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
for name, s := range project.Services {
|
||||
|
|
@ -86,7 +93,7 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project,
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service types.ServiceConfig) (types.Mapping, error) {
|
||||
func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service types.ServiceConfig) (types.Mapping, error) { //nolint:gocyclo
|
||||
var action string
|
||||
switch command {
|
||||
case "up":
|
||||
|
|
@ -95,6 +102,9 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
|
|||
case "down":
|
||||
s.events.On(removingEvent(service.Name))
|
||||
action = "remove"
|
||||
case "stop":
|
||||
s.events.On(stoppingEvent(service.Name))
|
||||
action = "stop"
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported plugin command: %s", command)
|
||||
}
|
||||
|
|
@ -152,6 +162,8 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
|
|||
s.events.On(createdEvent(service.Name))
|
||||
case "down":
|
||||
s.events.On(removedEvent(service.Name))
|
||||
case "stop":
|
||||
s.events.On(stoppedEvent(service.Name))
|
||||
}
|
||||
return variables, nil
|
||||
}
|
||||
|
|
@ -178,6 +190,11 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
|
|||
currentCommandMetadata = cmdOptionsMetadata.Up
|
||||
case "down":
|
||||
currentCommandMetadata = cmdOptionsMetadata.Down
|
||||
case "stop":
|
||||
if cmdOptionsMetadata.Stop == nil {
|
||||
return nil, nil
|
||||
}
|
||||
currentCommandMetadata = *cmdOptionsMetadata.Stop
|
||||
}
|
||||
|
||||
provider := *service.Provider
|
||||
|
|
@ -241,9 +258,10 @@ func (s *composeService) getPluginMetadata(path, command string, project *types.
|
|||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
func (p ProviderMetadata) IsEmpty() bool {
|
||||
|
|
|
|||
107
pkg/compose/plugins_test.go
Normal file
107
pkg/compose/plugins_test.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestProviderMetadata_IsEmpty(t *testing.T) {
|
||||
param := []ParameterMetadata{{Name: "x"}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
metadata ProviderMetadata
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty metadata",
|
||||
metadata: ProviderMetadata{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "only Description set",
|
||||
metadata: ProviderMetadata{Description: "something"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "only Up.Parameters set",
|
||||
metadata: ProviderMetadata{Up: CommandMetadata{Parameters: param}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "only Down.Parameters set",
|
||||
metadata: ProviderMetadata{Down: CommandMetadata{Parameters: param}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "only Stop set is empty",
|
||||
metadata: ProviderMetadata{Stop: &CommandMetadata{}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "all fields set",
|
||||
metadata: ProviderMetadata{
|
||||
Description: "full",
|
||||
Up: CommandMetadata{Parameters: param},
|
||||
Down: CommandMetadata{Parameters: param},
|
||||
Stop: &CommandMetadata{Parameters: param},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.metadata.IsEmpty(), tc.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderMetadata_JSONUnmarshal(t *testing.T) {
|
||||
raw := `{"description":"x","up":{"parameters":[{"name":"a"}]},"down":{"parameters":[{"name":"b"}]},"stop":{"parameters":[{"name":"c"}]}}`
|
||||
|
||||
var metadata ProviderMetadata
|
||||
err := json.Unmarshal([]byte(raw), &metadata)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, metadata.Description, "x")
|
||||
assert.Equal(t, metadata.Up.Parameters[0].Name, "a")
|
||||
assert.Equal(t, metadata.Down.Parameters[0].Name, "b")
|
||||
assert.Assert(t, metadata.Stop != nil, "Stop should be non-nil when present in JSON")
|
||||
assert.Equal(t, metadata.Stop.Parameters[0].Name, "c")
|
||||
}
|
||||
|
||||
func TestProviderMetadata_StopAbsent(t *testing.T) {
|
||||
raw := `{"description":"x","up":{"parameters":[]},"down":{"parameters":[]}}`
|
||||
|
||||
var metadata ProviderMetadata
|
||||
err := json.Unmarshal([]byte(raw), &metadata)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, metadata.Stop == nil, "Stop should be nil when absent from JSON")
|
||||
}
|
||||
|
||||
func TestProviderMetadata_StopAdvertisedWithoutParameters(t *testing.T) {
|
||||
raw := `{"stop":{"parameters":null}}`
|
||||
|
||||
var metadata ProviderMetadata
|
||||
err := json.Unmarshal([]byte(raw), &metadata)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, metadata.Stop != nil, "Stop should be non-nil when key present even with null parameters")
|
||||
}
|
||||
|
|
@ -53,6 +53,9 @@ func (s *composeService) stop(ctx context.Context, projectName string, options a
|
|||
return nil
|
||||
}
|
||||
serv := project.Services[service]
|
||||
if serv.Provider != nil {
|
||||
return s.runPlugin(ctx, project, serv, "stop")
|
||||
}
|
||||
return s.stopContainers(ctx, &serv, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout, event)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
13
pkg/e2e/fixtures/providers/provider-stop.yaml
Normal file
13
pkg/e2e/fixtures/providers/provider-stop.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
services:
|
||||
test:
|
||||
image: alpine
|
||||
command: echo ok
|
||||
depends_on:
|
||||
- provider
|
||||
provider:
|
||||
provider:
|
||||
type: example-provider
|
||||
options:
|
||||
name: provider
|
||||
type: test
|
||||
size: 1
|
||||
|
|
@ -29,6 +29,37 @@ import (
|
|||
"gotest.tools/v3/icmd"
|
||||
)
|
||||
|
||||
// TestProviderStopHook verifies that "docker compose stop" invokes the provider
|
||||
// binary's "stop" subcommand. The example provider writes a sentinel file at
|
||||
// PROVIDER_STOP_MARKER when its stop subcommand runs.
|
||||
func TestProviderStopHook(t *testing.T) {
|
||||
provider, err := findExecutable("example-provider")
|
||||
assert.NilError(t, err)
|
||||
|
||||
markerFile := filepath.Join(t.TempDir(), "example-provider-stop-marker")
|
||||
|
||||
path := fmt.Sprintf("%s%s%s", os.Getenv("PATH"), string(os.PathListSeparator), filepath.Dir(provider))
|
||||
c := NewParallelCLI(t, WithEnv(
|
||||
"PATH="+path,
|
||||
"PROVIDER_STOP_MARKER="+markerFile,
|
||||
))
|
||||
const projectName = "provider-stop-hook"
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = os.Remove(markerFile)
|
||||
c.cleanupWithDown(t, projectName)
|
||||
})
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "fixtures/providers/provider-stop.yaml", "--project-name", projectName, "up", "-d")
|
||||
res.Assert(t, icmd.Success)
|
||||
|
||||
res = c.RunDockerComposeCmd(t, "-f", "fixtures/providers/provider-stop.yaml", "--project-name", projectName, "stop")
|
||||
res.Assert(t, icmd.Success)
|
||||
|
||||
_, statErr := os.Stat(markerFile)
|
||||
assert.NilError(t, statErr, "expected example-provider stop subcommand to write marker file %q", markerFile)
|
||||
}
|
||||
|
||||
func TestDependsOnMultipleProviders(t *testing.T) {
|
||||
provider, err := findExecutable("example-provider")
|
||||
assert.NilError(t, err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue