run.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package compose
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "os"
  19. "os/signal"
  20. "slices"
  21. "github.com/compose-spec/compose-go/v2/types"
  22. "github.com/docker/cli/cli"
  23. cmd "github.com/docker/cli/cli/command/container"
  24. "github.com/moby/moby/api/types/container"
  25. "github.com/moby/moby/api/types/events"
  26. "github.com/moby/moby/client"
  27. "github.com/moby/moby/client/pkg/stringid"
  28. "github.com/docker/compose/v5/pkg/api"
  29. )
  30. type prepareRunResult struct {
  31. containerID string
  32. service types.ServiceConfig
  33. created container.Summary
  34. }
  35. func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
  36. result, err := s.prepareRun(ctx, project, opts)
  37. if err != nil {
  38. return 0, err
  39. }
  40. // remove cancellable context signal handler so we can forward signals to container without compose from exiting
  41. signal.Reset()
  42. sigc := make(chan os.Signal, 128)
  43. signal.Notify(sigc)
  44. go cmd.ForwardAllSignals(ctx, s.apiClient(), result.containerID, sigc)
  45. defer signal.Stop(sigc)
  46. // If the service has post_start hooks, set up a goroutine that waits for
  47. // the container to start and then executes them. This is needed because
  48. // cmd.RunStart both starts and attaches to the container in one call,
  49. // so we can't run hooks sequentially between start and attach.
  50. var hookErrCh chan error
  51. if len(result.service.PostStart) > 0 {
  52. hookErrCh = make(chan error, 1)
  53. go func() {
  54. hookErrCh <- s.runPostStartHooksOnEvent(ctx, result.containerID, result.service, result.created)
  55. }()
  56. }
  57. err = cmd.RunStart(ctx, s.dockerCli, &cmd.StartOptions{
  58. OpenStdin: !opts.Detach && opts.Interactive,
  59. Attach: !opts.Detach,
  60. Containers: []string{result.containerID},
  61. DetachKeys: s.configFile().DetachKeys,
  62. })
  63. // Wait for hooks to complete if they were started
  64. if hookErrCh != nil {
  65. if hookErr := <-hookErrCh; hookErr != nil && err == nil {
  66. err = hookErr
  67. }
  68. }
  69. var stErr cli.StatusError
  70. if errors.As(err, &stErr) {
  71. return stErr.StatusCode, nil
  72. }
  73. return 0, err
  74. }
  75. // runPostStartHooksOnEvent listens for the container's start event and executes
  76. // post_start lifecycle hooks once the container is running.
  77. func (s *composeService) runPostStartHooksOnEvent(ctx context.Context, containerID string, service types.ServiceConfig, ctr container.Summary) error {
  78. evtCtx, cancel := context.WithCancel(ctx)
  79. defer cancel()
  80. res := s.apiClient().Events(evtCtx, client.EventsListOptions{
  81. Filters: make(client.Filters).
  82. Add("type", "container").
  83. Add("container", containerID).
  84. Add("event", string(events.ActionStart)),
  85. })
  86. // Wait for the container start event
  87. select {
  88. case <-evtCtx.Done():
  89. return evtCtx.Err()
  90. case err := <-res.Err:
  91. return err
  92. case <-res.Messages:
  93. // Container started, run hooks
  94. }
  95. for _, hook := range service.PostStart {
  96. if err := s.runHook(ctx, ctr, service, hook, nil); err != nil {
  97. return err
  98. }
  99. }
  100. return nil
  101. }
  102. func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (prepareRunResult, error) {
  103. // Temporary implementation of use_api_socket until we get actual support inside docker engine
  104. project, err := s.useAPISocket(project)
  105. if err != nil {
  106. return prepareRunResult{}, err
  107. }
  108. err = Run(ctx, func(ctx context.Context) error {
  109. return s.startDependencies(ctx, project, opts)
  110. }, "run", s.events)
  111. if err != nil {
  112. return prepareRunResult{}, err
  113. }
  114. service, err := project.GetService(opts.Service)
  115. if err != nil {
  116. return prepareRunResult{}, err
  117. }
  118. applyRunOptions(project, &service, opts)
  119. if err := s.stdin().CheckTty(opts.Interactive, service.Tty); err != nil {
  120. return prepareRunResult{}, err
  121. }
  122. slug := stringid.GenerateRandomID()
  123. if service.ContainerName == "" {
  124. service.ContainerName = fmt.Sprintf("%[1]s%[4]s%[2]s%[4]srun%[4]s%[3]s", project.Name, service.Name, stringid.TruncateID(slug), api.Separator)
  125. }
  126. one := 1
  127. service.Scale = &one
  128. service.Restart = ""
  129. if service.Deploy != nil {
  130. service.Deploy.RestartPolicy = nil
  131. }
  132. service.CustomLabels = service.CustomLabels.
  133. Add(api.SlugLabel, slug).
  134. Add(api.OneoffLabel, "True")
  135. // Only ensure image exists for the target service, dependencies were already handled by startDependencies
  136. buildOpts := prepareBuildOptions(opts)
  137. if err := s.ensureImagesExists(ctx, project, buildOpts, opts.QuietPull); err != nil { // all dependencies already checked, but might miss service img
  138. return prepareRunResult{}, err
  139. }
  140. observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true)
  141. if err != nil {
  142. return prepareRunResult{}, err
  143. }
  144. if !opts.NoDeps {
  145. if err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, observedState, 0); err != nil {
  146. return prepareRunResult{}, err
  147. }
  148. }
  149. createOpts := createOptions{
  150. AutoRemove: opts.AutoRemove,
  151. AttachStdin: opts.Interactive,
  152. UseNetworkAliases: opts.UseNetworkAliases,
  153. Labels: mergeLabels(service.Labels, service.CustomLabels),
  154. }
  155. err = newConvergence(project.ServiceNames(), observedState, nil, nil, s).resolveServiceReferences(&service)
  156. if err != nil {
  157. return prepareRunResult{}, err
  158. }
  159. err = s.ensureModels(ctx, project, opts.QuietPull)
  160. if err != nil {
  161. return prepareRunResult{}, err
  162. }
  163. created, err := s.createContainer(ctx, project, service, service.ContainerName, -1, createOpts)
  164. if err != nil {
  165. return prepareRunResult{}, err
  166. }
  167. inspect, err := s.apiClient().ContainerInspect(ctx, created.ID, client.ContainerInspectOptions{})
  168. if err != nil {
  169. return prepareRunResult{}, err
  170. }
  171. err = s.injectSecrets(ctx, project, service, inspect.Container.ID)
  172. if err != nil {
  173. return prepareRunResult{containerID: created.ID}, err
  174. }
  175. err = s.injectConfigs(ctx, project, service, inspect.Container.ID)
  176. return prepareRunResult{
  177. containerID: created.ID,
  178. service: service,
  179. created: created,
  180. }, err
  181. }
  182. func prepareBuildOptions(opts api.RunOptions) *api.BuildOptions {
  183. if opts.Build == nil {
  184. return nil
  185. }
  186. // Create a copy of build options and restrict to only the target service
  187. buildOptsCopy := *opts.Build
  188. buildOptsCopy.Services = []string{opts.Service}
  189. return &buildOptsCopy
  190. }
  191. func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) {
  192. service.Tty = opts.Tty
  193. service.StdinOpen = opts.Interactive
  194. service.ContainerName = opts.Name
  195. if len(opts.Command) > 0 {
  196. service.Command = opts.Command
  197. }
  198. if opts.User != "" {
  199. service.User = opts.User
  200. }
  201. if len(opts.CapAdd) > 0 {
  202. service.CapAdd = append(service.CapAdd, opts.CapAdd...)
  203. service.CapDrop = slices.DeleteFunc(service.CapDrop, func(e string) bool { return slices.Contains(opts.CapAdd, e) })
  204. }
  205. if len(opts.CapDrop) > 0 {
  206. service.CapDrop = append(service.CapDrop, opts.CapDrop...)
  207. service.CapAdd = slices.DeleteFunc(service.CapAdd, func(e string) bool { return slices.Contains(opts.CapDrop, e) })
  208. }
  209. if opts.WorkingDir != "" {
  210. service.WorkingDir = opts.WorkingDir
  211. }
  212. if opts.Entrypoint != nil {
  213. service.Entrypoint = opts.Entrypoint
  214. if len(opts.Command) == 0 {
  215. service.Command = []string{}
  216. }
  217. }
  218. if len(opts.Environment) > 0 {
  219. cmdEnv := types.NewMappingWithEquals(opts.Environment)
  220. serviceOverrideEnv := cmdEnv.Resolve(func(s string) (string, bool) {
  221. v, ok := envResolver(project.Environment)(s)
  222. return v, ok
  223. }).RemoveEmpty()
  224. if service.Environment == nil {
  225. service.Environment = types.MappingWithEquals{}
  226. }
  227. service.Environment.OverrideBy(serviceOverrideEnv)
  228. }
  229. for k, v := range opts.Labels {
  230. service.Labels = service.Labels.Add(k, v)
  231. }
  232. }
  233. func (s *composeService) startDependencies(ctx context.Context, project *types.Project, options api.RunOptions) error {
  234. project = project.WithServicesDisabled(options.Service)
  235. err := s.Create(ctx, project, api.CreateOptions{
  236. Build: options.Build,
  237. IgnoreOrphans: options.IgnoreOrphans,
  238. RemoveOrphans: options.RemoveOrphans,
  239. QuietPull: options.QuietPull,
  240. })
  241. if err != nil {
  242. return err
  243. }
  244. if len(project.Services) > 0 {
  245. return s.Start(ctx, project.Name, api.StartOptions{
  246. Project: project,
  247. })
  248. }
  249. return nil
  250. }