compose.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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 := c.Labels[api.ServiceLabel]
  154. service, ok := set[serviceLabel]
  155. if !ok {
  156. service = types.ServiceConfig{
  157. Name: serviceLabel,
  158. Image: c.Image,
  159. Labels: c.Labels,
  160. }
  161. }
  162. service.Scale = increment(service.Scale)
  163. set[serviceLabel] = service
  164. }
  165. for name, service := range set {
  166. dependencies := service.Labels[api.DependenciesLabel]
  167. if len(dependencies) > 0 {
  168. service.DependsOn = types.DependsOnConfig{}
  169. for _, dc := range strings.Split(dependencies, ",") {
  170. dcArr := strings.Split(dc, ":")
  171. condition := ServiceConditionRunningOrHealthy
  172. // Let's restart the dependency by default if we don't have the info stored in the label
  173. restart := true
  174. required := true
  175. dependency := dcArr[0]
  176. // backward compatibility
  177. if len(dcArr) > 1 {
  178. condition = dcArr[1]
  179. if len(dcArr) > 2 {
  180. restart, _ = strconv.ParseBool(dcArr[2])
  181. }
  182. }
  183. service.DependsOn[dependency] = types.ServiceDependency{Condition: condition, Restart: restart, Required: required}
  184. }
  185. set[name] = service
  186. }
  187. }
  188. project.Services = set
  189. SERVICES:
  190. for _, qs := range services {
  191. for _, es := range project.Services {
  192. if es.Name == qs {
  193. continue SERVICES
  194. }
  195. }
  196. return project, fmt.Errorf("no such service: %q: %w", qs, api.ErrNotFound)
  197. }
  198. project, err := project.WithSelectedServices(services)
  199. if err != nil {
  200. return project, err
  201. }
  202. return project, nil
  203. }
  204. func increment(scale *int) *int {
  205. i := 1
  206. if scale != nil {
  207. i = *scale + 1
  208. }
  209. return &i
  210. }
  211. func (s *composeService) actualVolumes(ctx context.Context, projectName string) (types.Volumes, error) {
  212. opts := volume.ListOptions{
  213. Filters: filters.NewArgs(projectFilter(projectName)),
  214. }
  215. volumes, err := s.apiClient().VolumeList(ctx, opts)
  216. if err != nil {
  217. return nil, err
  218. }
  219. actual := types.Volumes{}
  220. for _, vol := range volumes.Volumes {
  221. actual[vol.Labels[api.VolumeLabel]] = types.VolumeConfig{
  222. Name: vol.Name,
  223. Driver: vol.Driver,
  224. Labels: vol.Labels,
  225. }
  226. }
  227. return actual, nil
  228. }
  229. func (s *composeService) actualNetworks(ctx context.Context, projectName string) (types.Networks, error) {
  230. networks, err := s.apiClient().NetworkList(ctx, network.ListOptions{
  231. Filters: filters.NewArgs(projectFilter(projectName)),
  232. })
  233. if err != nil {
  234. return nil, err
  235. }
  236. actual := types.Networks{}
  237. for _, net := range networks {
  238. actual[net.Labels[api.NetworkLabel]] = types.NetworkConfig{
  239. Name: net.Name,
  240. Driver: net.Driver,
  241. Labels: net.Labels,
  242. }
  243. }
  244. return actual, nil
  245. }
  246. var swarmEnabled = struct {
  247. once sync.Once
  248. val bool
  249. err error
  250. }{}
  251. func (s *composeService) isSWarmEnabled(ctx context.Context) (bool, error) {
  252. swarmEnabled.once.Do(func() {
  253. info, err := s.apiClient().Info(ctx)
  254. if err != nil {
  255. swarmEnabled.err = err
  256. }
  257. switch info.Swarm.LocalNodeState {
  258. case swarm.LocalNodeStateInactive, swarm.LocalNodeStateLocked:
  259. swarmEnabled.val = false
  260. default:
  261. swarmEnabled.val = true
  262. }
  263. })
  264. return swarmEnabled.val, swarmEnabled.err
  265. }
  266. type runtimeVersionCache struct {
  267. once sync.Once
  268. val string
  269. err error
  270. }
  271. var runtimeVersion runtimeVersionCache
  272. func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
  273. runtimeVersion.once.Do(func() {
  274. version, err := s.dockerCli.Client().ServerVersion(ctx)
  275. if err != nil {
  276. runtimeVersion.err = err
  277. }
  278. runtimeVersion.val = version.APIVersion
  279. })
  280. return runtimeVersion.val, runtimeVersion.err
  281. }
  282. func (s *composeService) isDesktopIntegrationActive() bool {
  283. return s.desktopCli != nil
  284. }
  285. func (s *composeService) isDesktopUIEnabled() bool {
  286. if !s.isDesktopIntegrationActive() {
  287. return false
  288. }
  289. return s.experiments.ComposeUI()
  290. }