mirror of
https://github.com/docker/compose.git
synced 2026-05-13 13:58:02 +00:00
fix: execute post_start hooks in docker compose run
RunOneOffContainer was not executing post_start lifecycle hooks after starting a container. This adds hook execution by listening for the container's start event via the Docker Events API and running hooks once the container is running, matching the behavior already present in startService (used by docker compose up) and restart. Signed-off-by: Varun Chawla <varun_6april@hotmail.com>
This commit is contained in:
parent
f9828dfab9
commit
81d7d3c60b
1 changed files with 80 additions and 17 deletions
|
|
@ -27,14 +27,22 @@ import (
|
|||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli"
|
||||
cmd "github.com/docker/cli/cli/command/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/events"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/moby/client/pkg/stringid"
|
||||
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
||||
type prepareRunResult struct {
|
||||
containerID string
|
||||
service types.ServiceConfig
|
||||
created container.Summary
|
||||
}
|
||||
|
||||
func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
|
||||
containerID, err := s.prepareRun(ctx, project, opts)
|
||||
result, err := s.prepareRun(ctx, project, opts)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -44,15 +52,35 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
|||
|
||||
sigc := make(chan os.Signal, 128)
|
||||
signal.Notify(sigc)
|
||||
go cmd.ForwardAllSignals(ctx, s.apiClient(), containerID, sigc)
|
||||
go cmd.ForwardAllSignals(ctx, s.apiClient(), result.containerID, sigc)
|
||||
defer signal.Stop(sigc)
|
||||
|
||||
// If the service has post_start hooks, set up a goroutine that waits for
|
||||
// the container to start and then executes them. This is needed because
|
||||
// cmd.RunStart both starts and attaches to the container in one call,
|
||||
// so we can't run hooks sequentially between start and attach.
|
||||
var hookErrCh chan error
|
||||
if len(result.service.PostStart) > 0 {
|
||||
hookErrCh = make(chan error, 1)
|
||||
go func() {
|
||||
hookErrCh <- s.runPostStartHooksOnEvent(ctx, result.containerID, result.service, result.created)
|
||||
}()
|
||||
}
|
||||
|
||||
err = cmd.RunStart(ctx, s.dockerCli, &cmd.StartOptions{
|
||||
OpenStdin: !opts.Detach && opts.Interactive,
|
||||
Attach: !opts.Detach,
|
||||
Containers: []string{containerID},
|
||||
Containers: []string{result.containerID},
|
||||
DetachKeys: s.configFile().DetachKeys,
|
||||
})
|
||||
|
||||
// Wait for hooks to complete if they were started
|
||||
if hookErrCh != nil {
|
||||
if hookErr := <-hookErrCh; hookErr != nil && err == nil {
|
||||
err = hookErr
|
||||
}
|
||||
}
|
||||
|
||||
var stErr cli.StatusError
|
||||
if errors.As(err, &stErr) {
|
||||
return stErr.StatusCode, nil
|
||||
|
|
@ -60,29 +88,60 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
|||
return 0, err
|
||||
}
|
||||
|
||||
func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) {
|
||||
// runPostStartHooksOnEvent listens for the container's start event and executes
|
||||
// post_start lifecycle hooks once the container is running.
|
||||
func (s *composeService) runPostStartHooksOnEvent(ctx context.Context, containerID string, service types.ServiceConfig, ctr container.Summary) error {
|
||||
evtCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
res := s.apiClient().Events(evtCtx, client.EventsListOptions{
|
||||
Filters: make(client.Filters).
|
||||
Add("type", "container").
|
||||
Add("container", containerID).
|
||||
Add("event", string(events.ActionStart)),
|
||||
})
|
||||
|
||||
// Wait for the container start event
|
||||
select {
|
||||
case <-evtCtx.Done():
|
||||
return evtCtx.Err()
|
||||
case err := <-res.Err:
|
||||
return err
|
||||
case <-res.Messages:
|
||||
// Container started, run hooks
|
||||
}
|
||||
|
||||
for _, hook := range service.PostStart {
|
||||
if err := s.runHook(ctx, ctr, service, hook, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (prepareRunResult, error) {
|
||||
// Temporary implementation of use_api_socket until we get actual support inside docker engine
|
||||
project, err := s.useAPISocket(project)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return prepareRunResult{}, err
|
||||
}
|
||||
|
||||
err = Run(ctx, func(ctx context.Context) error {
|
||||
return s.startDependencies(ctx, project, opts)
|
||||
}, "run", s.events)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return prepareRunResult{}, err
|
||||
}
|
||||
|
||||
service, err := project.GetService(opts.Service)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return prepareRunResult{}, err
|
||||
}
|
||||
|
||||
applyRunOptions(project, &service, opts)
|
||||
|
||||
if err := s.stdin().CheckTty(opts.Interactive, service.Tty); err != nil {
|
||||
return "", err
|
||||
return prepareRunResult{}, err
|
||||
}
|
||||
|
||||
slug := stringid.GenerateRandomID()
|
||||
|
|
@ -102,17 +161,17 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
|
|||
// Only ensure image exists for the target service, dependencies were already handled by startDependencies
|
||||
buildOpts := prepareBuildOptions(opts)
|
||||
if err := s.ensureImagesExists(ctx, project, buildOpts, opts.QuietPull); err != nil { // all dependencies already checked, but might miss service img
|
||||
return "", err
|
||||
return prepareRunResult{}, err
|
||||
}
|
||||
|
||||
observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return prepareRunResult{}, err
|
||||
}
|
||||
|
||||
if !opts.NoDeps {
|
||||
if err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, observedState, 0); err != nil {
|
||||
return "", err
|
||||
return prepareRunResult{}, err
|
||||
}
|
||||
}
|
||||
createOpts := createOptions{
|
||||
|
|
@ -124,31 +183,35 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
|
|||
|
||||
err = newConvergence(project.ServiceNames(), observedState, nil, nil, s).resolveServiceReferences(&service)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return prepareRunResult{}, err
|
||||
}
|
||||
|
||||
err = s.ensureModels(ctx, project, opts.QuietPull)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return prepareRunResult{}, err
|
||||
}
|
||||
|
||||
created, err := s.createContainer(ctx, project, service, service.ContainerName, -1, createOpts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return prepareRunResult{}, err
|
||||
}
|
||||
|
||||
inspect, err := s.apiClient().ContainerInspect(ctx, created.ID, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
return prepareRunResult{}, err
|
||||
}
|
||||
|
||||
err = s.injectSecrets(ctx, project, service, inspect.Container.ID)
|
||||
if err != nil {
|
||||
return created.ID, err
|
||||
return prepareRunResult{containerID: created.ID}, err
|
||||
}
|
||||
|
||||
err = s.injectConfigs(ctx, project, service, inspect.Container.ID)
|
||||
return created.ID, err
|
||||
return prepareRunResult{
|
||||
containerID: created.ID,
|
||||
service: service,
|
||||
created: created,
|
||||
}, err
|
||||
}
|
||||
|
||||
func prepareBuildOptions(opts api.RunOptions) *api.BuildOptions {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue