Skip to content

Commit 81d7d3c

Browse files
veeceeyndeloof
authored andcommitted
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>
1 parent f9828df commit 81d7d3c

1 file changed

Lines changed: 80 additions & 17 deletions

File tree

pkg/compose/run.go

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,22 @@ import (
2727
"github.com/compose-spec/compose-go/v2/types"
2828
"github.com/docker/cli/cli"
2929
cmd "github.com/docker/cli/cli/command/container"
30+
"github.com/moby/moby/api/types/container"
31+
"github.com/moby/moby/api/types/events"
3032
"github.com/moby/moby/client"
3133
"github.com/moby/moby/client/pkg/stringid"
3234

3335
"github.com/docker/compose/v5/pkg/api"
3436
)
3537

38+
type prepareRunResult struct {
39+
containerID string
40+
service types.ServiceConfig
41+
created container.Summary
42+
}
43+
3644
func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
37-
containerID, err := s.prepareRun(ctx, project, opts)
45+
result, err := s.prepareRun(ctx, project, opts)
3846
if err != nil {
3947
return 0, err
4048
}
@@ -44,45 +52,96 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
4452

4553
sigc := make(chan os.Signal, 128)
4654
signal.Notify(sigc)
47-
go cmd.ForwardAllSignals(ctx, s.apiClient(), containerID, sigc)
55+
go cmd.ForwardAllSignals(ctx, s.apiClient(), result.containerID, sigc)
4856
defer signal.Stop(sigc)
4957

58+
// If the service has post_start hooks, set up a goroutine that waits for
59+
// the container to start and then executes them. This is needed because
60+
// cmd.RunStart both starts and attaches to the container in one call,
61+
// so we can't run hooks sequentially between start and attach.
62+
var hookErrCh chan error
63+
if len(result.service.PostStart) > 0 {
64+
hookErrCh = make(chan error, 1)
65+
go func() {
66+
hookErrCh <- s.runPostStartHooksOnEvent(ctx, result.containerID, result.service, result.created)
67+
}()
68+
}
69+
5070
err = cmd.RunStart(ctx, s.dockerCli, &cmd.StartOptions{
5171
OpenStdin: !opts.Detach && opts.Interactive,
5272
Attach: !opts.Detach,
53-
Containers: []string{containerID},
73+
Containers: []string{result.containerID},
5474
DetachKeys: s.configFile().DetachKeys,
5575
})
76+
77+
// Wait for hooks to complete if they were started
78+
if hookErrCh != nil {
79+
if hookErr := <-hookErrCh; hookErr != nil && err == nil {
80+
err = hookErr
81+
}
82+
}
83+
5684
var stErr cli.StatusError
5785
if errors.As(err, &stErr) {
5886
return stErr.StatusCode, nil
5987
}
6088
return 0, err
6189
}
6290

63-
func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) {
91+
// runPostStartHooksOnEvent listens for the container's start event and executes
92+
// post_start lifecycle hooks once the container is running.
93+
func (s *composeService) runPostStartHooksOnEvent(ctx context.Context, containerID string, service types.ServiceConfig, ctr container.Summary) error {
94+
evtCtx, cancel := context.WithCancel(ctx)
95+
defer cancel()
96+
97+
res := s.apiClient().Events(evtCtx, client.EventsListOptions{
98+
Filters: make(client.Filters).
99+
Add("type", "container").
100+
Add("container", containerID).
101+
Add("event", string(events.ActionStart)),
102+
})
103+
104+
// Wait for the container start event
105+
select {
106+
case <-evtCtx.Done():
107+
return evtCtx.Err()
108+
case err := <-res.Err:
109+
return err
110+
case <-res.Messages:
111+
// Container started, run hooks
112+
}
113+
114+
for _, hook := range service.PostStart {
115+
if err := s.runHook(ctx, ctr, service, hook, nil); err != nil {
116+
return err
117+
}
118+
}
119+
return nil
120+
}
121+
122+
func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (prepareRunResult, error) {
64123
// Temporary implementation of use_api_socket until we get actual support inside docker engine
65124
project, err := s.useAPISocket(project)
66125
if err != nil {
67-
return "", err
126+
return prepareRunResult{}, err
68127
}
69128

70129
err = Run(ctx, func(ctx context.Context) error {
71130
return s.startDependencies(ctx, project, opts)
72131
}, "run", s.events)
73132
if err != nil {
74-
return "", err
133+
return prepareRunResult{}, err
75134
}
76135

77136
service, err := project.GetService(opts.Service)
78137
if err != nil {
79-
return "", err
138+
return prepareRunResult{}, err
80139
}
81140

82141
applyRunOptions(project, &service, opts)
83142

84143
if err := s.stdin().CheckTty(opts.Interactive, service.Tty); err != nil {
85-
return "", err
144+
return prepareRunResult{}, err
86145
}
87146

88147
slug := stringid.GenerateRandomID()
@@ -102,17 +161,17 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
102161
// Only ensure image exists for the target service, dependencies were already handled by startDependencies
103162
buildOpts := prepareBuildOptions(opts)
104163
if err := s.ensureImagesExists(ctx, project, buildOpts, opts.QuietPull); err != nil { // all dependencies already checked, but might miss service img
105-
return "", err
164+
return prepareRunResult{}, err
106165
}
107166

108167
observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true)
109168
if err != nil {
110-
return "", err
169+
return prepareRunResult{}, err
111170
}
112171

113172
if !opts.NoDeps {
114173
if err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, observedState, 0); err != nil {
115-
return "", err
174+
return prepareRunResult{}, err
116175
}
117176
}
118177
createOpts := createOptions{
@@ -124,31 +183,35 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
124183

125184
err = newConvergence(project.ServiceNames(), observedState, nil, nil, s).resolveServiceReferences(&service)
126185
if err != nil {
127-
return "", err
186+
return prepareRunResult{}, err
128187
}
129188

130189
err = s.ensureModels(ctx, project, opts.QuietPull)
131190
if err != nil {
132-
return "", err
191+
return prepareRunResult{}, err
133192
}
134193

135194
created, err := s.createContainer(ctx, project, service, service.ContainerName, -1, createOpts)
136195
if err != nil {
137-
return "", err
196+
return prepareRunResult{}, err
138197
}
139198

140199
inspect, err := s.apiClient().ContainerInspect(ctx, created.ID, client.ContainerInspectOptions{})
141200
if err != nil {
142-
return "", err
201+
return prepareRunResult{}, err
143202
}
144203

145204
err = s.injectSecrets(ctx, project, service, inspect.Container.ID)
146205
if err != nil {
147-
return created.ID, err
206+
return prepareRunResult{containerID: created.ID}, err
148207
}
149208

150209
err = s.injectConfigs(ctx, project, service, inspect.Container.ID)
151-
return created.ID, err
210+
return prepareRunResult{
211+
containerID: created.ID,
212+
service: service,
213+
created: created,
214+
}, err
152215
}
153216

154217
func prepareBuildOptions(opts api.RunOptions) *api.BuildOptions {

0 commit comments

Comments
 (0)