compose.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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. "strconv"
  20. "strings"
  21. "sync"
  22. "github.com/docker/compose/v2/internal/desktop"
  23. "github.com/docker/compose/v2/internal/experimental"
  24. "github.com/docker/docker/api/types/network"
  25. "github.com/docker/docker/api/types/volume"
  26. "github.com/jonboulle/clockwork"
  27. "github.com/compose-spec/compose-go/v2/types"
  28. "github.com/docker/cli/cli/command"
  29. "github.com/docker/cli/cli/config/configfile"
  30. "github.com/docker/cli/cli/flags"
  31. "github.com/docker/cli/cli/streams"
  32. "github.com/docker/compose/v2/pkg/api"
  33. moby "github.com/docker/docker/api/types"
  34. "github.com/docker/docker/api/types/filters"
  35. "github.com/docker/docker/api/types/swarm"
  36. "github.com/docker/docker/client"
  37. )
  38. var stdioToStdout bool
  39. func init() {
  40. out, ok := os.LookupEnv("COMPOSE_STATUS_STDOUT")
  41. if ok {
  42. stdioToStdout, _ = strconv.ParseBool(out)
  43. }
  44. }
  45. // NewComposeService create a local implementation of the compose.Service API
  46. func NewComposeService(dockerCli command.Cli) api.Service {
  47. return &composeService{
  48. dockerCli: dockerCli,
  49. clock: clockwork.NewRealClock(),
  50. maxConcurrency: -1,
  51. dryRun: false,
  52. }
  53. }
  54. type composeService struct {
  55. dockerCli command.Cli
  56. desktopCli *desktop.Client
  57. experiments *experimental.State
  58. clock clockwork.Clock
  59. maxConcurrency int
  60. dryRun bool
  61. }
  62. // Close releases any connections/resources held by the underlying clients.
  63. //
  64. // In practice, this service has the same lifetime as the process, so everything
  65. // will get cleaned up at about the same time regardless even if not invoked.
  66. func (s *composeService) Close() error {
  67. var errs []error
  68. if s.dockerCli != nil {
  69. errs = append(errs, s.dockerCli.Client().Close())
  70. }
  71. if s.isDesktopIntegrationActive() {
  72. errs = append(errs, s.desktopCli.Close())
  73. }
  74. return errors.Join(errs...)
  75. }
  76. func (s *composeService) apiClient() client.APIClient {
  77. return s.dockerCli.Client()
  78. }
  79. func (s *composeService) configFile() *configfile.ConfigFile {
  80. return s.dockerCli.ConfigFile()
  81. }
  82. func (s *composeService) MaxConcurrency(i int) {
  83. s.maxConcurrency = i
  84. }
  85. func (s *composeService) DryRunMode(ctx context.Context, dryRun bool) (context.Context, error) {
  86. s.dryRun = dryRun
  87. if dryRun {
  88. cli, err := command.NewDockerCli()
  89. if err != nil {
  90. return ctx, err
  91. }
  92. options := flags.NewClientOptions()
  93. options.Context = s.dockerCli.CurrentContext()
  94. err = cli.Initialize(options, command.WithInitializeClient(func(cli *command.DockerCli) (client.APIClient, error) {
  95. return api.NewDryRunClient(s.apiClient(), s.dockerCli)
  96. }))
  97. if err != nil {
  98. return ctx, err
  99. }
  100. s.dockerCli = cli
  101. }
  102. return context.WithValue(ctx, api.DryRunKey{}, dryRun), nil
  103. }
  104. func (s *composeService) stdout() *streams.Out {
  105. return s.dockerCli.Out()
  106. }
  107. func (s *composeService) stdin() *streams.In {
  108. return s.dockerCli.In()
  109. }
  110. func (s *composeService) stderr() *streams.Out {
  111. return s.dockerCli.Err()
  112. }
  113. func (s *composeService) stdinfo() *streams.Out {
  114. if stdioToStdout {
  115. return s.dockerCli.Out()
  116. }
  117. return s.dockerCli.Err()
  118. }
  119. func getCanonicalContainerName(c moby.Container) string {
  120. if len(c.Names) == 0 {
  121. // corner case, sometime happens on removal. return short ID as a safeguard value
  122. return c.ID[:12]
  123. }
  124. // Names return container canonical name /foo + link aliases /linked_by/foo
  125. for _, name := range c.Names {
  126. if strings.LastIndex(name, "/") == 0 {
  127. return name[1:]
  128. }
  129. }
  130. return strings.TrimPrefix(c.Names[0], "/")
  131. }
  132. func getContainerNameWithoutProject(c moby.Container) string {
  133. project := c.Labels[api.ProjectLabel]
  134. defaultName := getDefaultContainerName(project, c.Labels[api.ServiceLabel], c.Labels[api.ContainerNumberLabel])
  135. name := getCanonicalContainerName(c)
  136. if name != defaultName {
  137. // service declares a custom container_name
  138. return name
  139. }
  140. return name[len(project)+1:]
  141. }
  142. // projectFromName builds a types.Project based on actual resources with compose labels set
  143. func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) {
  144. project := &types.Project{
  145. Name: projectName,
  146. Services: types.Services{},
  147. }
  148. if len(containers) == 0 {
  149. return project, fmt.Errorf("no container found for project %q: %w", projectName, api.ErrNotFound)
  150. }
  151. set := types.Services{}
  152. for _, c := range containers {
  153. serviceLabel, ok := c.Labels[api.ServiceLabel]
  154. if !ok {
  155. serviceLabel = getCanonicalContainerName(c)
  156. }
  157. service, ok := set[serviceLabel]
  158. if !ok {
  159. service = types.ServiceConfig{
  160. Name: serviceLabel,
  161. Image: c.Image,
  162. Labels: c.Labels,
  163. }
  164. }
  165. service.Scale = increment(service.Scale)
  166. set[serviceLabel] = service
  167. }
  168. for name, service := range set {
  169. dependencies := service.Labels[api.DependenciesLabel]
  170. if dependencies != "" {
  171. service.DependsOn = types.DependsOnConfig{}
  172. for _, dc := range strings.Split(dependencies, ",") {
  173. dcArr := strings.Split(dc, ":")
  174. condition := ServiceConditionRunningOrHealthy
  175. // Let's restart the dependency by default if we don't have the info stored in the label
  176. restart := true
  177. required := true
  178. dependency := dcArr[0]
  179. // backward compatibility
  180. if len(dcArr) > 1 {
  181. condition = dcArr[1]
  182. if len(dcArr) > 2 {
  183. restart, _ = strconv.ParseBool(dcArr[2])
  184. }
  185. }
  186. service.DependsOn[dependency] = types.ServiceDependency{Condition: condition, Restart: restart, Required: required}
  187. }
  188. set[name] = service
  189. }
  190. }
  191. project.Services = set
  192. SERVICES:
  193. for _, qs := range services {
  194. for _, es := range project.Services {
  195. if es.Name == qs {
  196. continue SERVICES
  197. }
  198. }
  199. return project, fmt.Errorf("no such service: %q: %w", qs, api.ErrNotFound)
  200. }
  201. project, err := project.WithSelectedServices(services)
  202. if err != nil {
  203. return project, err
  204. }
  205. return project, nil
  206. }
  207. func increment(scale *int) *int {
  208. i := 1
  209. if scale != nil {
  210. i = *scale + 1
  211. }
  212. return &i
  213. }
  214. func (s *composeService) actualVolumes(ctx context.Context, projectName string) (types.Volumes, error) {
  215. opts := volume.ListOptions{
  216. Filters: filters.NewArgs(projectFilter(projectName)),
  217. }
  218. volumes, err := s.apiClient().VolumeList(ctx, opts)
  219. if err != nil {
  220. return nil, err
  221. }
  222. actual := types.Volumes{}
  223. for _, vol := range volumes.Volumes {
  224. actual[vol.Labels[api.VolumeLabel]] = types.VolumeConfig{
  225. Name: vol.Name,
  226. Driver: vol.Driver,
  227. Labels: vol.Labels,
  228. }
  229. }
  230. return actual, nil
  231. }
  232. func (s *composeService) actualNetworks(ctx context.Context, projectName string) (types.Networks, error) {
  233. networks, err := s.apiClient().NetworkList(ctx, network.ListOptions{
  234. Filters: filters.NewArgs(projectFilter(projectName)),
  235. })
  236. if err != nil {
  237. return nil, err
  238. }
  239. actual := types.Networks{}
  240. for _, net := range networks {
  241. actual[net.Labels[api.NetworkLabel]] = types.NetworkConfig{
  242. Name: net.Name,
  243. Driver: net.Driver,
  244. Labels: net.Labels,
  245. }
  246. }
  247. return actual, nil
  248. }
  249. var swarmEnabled = struct {
  250. once sync.Once
  251. val bool
  252. err error
  253. }{}
  254. func (s *composeService) isSWarmEnabled(ctx context.Context) (bool, error) {
  255. swarmEnabled.once.Do(func() {
  256. info, err := s.apiClient().Info(ctx)
  257. if err != nil {
  258. swarmEnabled.err = err
  259. }
  260. switch info.Swarm.LocalNodeState {
  261. case swarm.LocalNodeStateInactive, swarm.LocalNodeStateLocked:
  262. swarmEnabled.val = false
  263. default:
  264. swarmEnabled.val = true
  265. }
  266. })
  267. return swarmEnabled.val, swarmEnabled.err
  268. }
  269. type runtimeVersionCache struct {
  270. once sync.Once
  271. val string
  272. err error
  273. }
  274. var runtimeVersion runtimeVersionCache
  275. func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
  276. runtimeVersion.once.Do(func() {
  277. version, err := s.dockerCli.Client().ServerVersion(ctx)
  278. if err != nil {
  279. runtimeVersion.err = err
  280. }
  281. runtimeVersion.val = version.APIVersion
  282. })
  283. return runtimeVersion.val, runtimeVersion.err
  284. }
  285. func (s *composeService) isDesktopIntegrationActive() bool {
  286. return s.desktopCli != nil
  287. }