@@ -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+
3644func (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
154217func prepareBuildOptions (opts api.RunOptions ) * api.BuildOptions {
0 commit comments