compose.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  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. "io"
  19. "os"
  20. "strconv"
  21. "strings"
  22. "sync"
  23. "github.com/compose-spec/compose-go/v2/types"
  24. "github.com/docker/buildx/store/storeutil"
  25. "github.com/docker/cli/cli/command"
  26. "github.com/docker/cli/cli/config/configfile"
  27. "github.com/docker/cli/cli/flags"
  28. "github.com/docker/cli/cli/streams"
  29. "github.com/docker/compose/v2/pkg/progress"
  30. "github.com/docker/docker/api/types/container"
  31. "github.com/docker/docker/api/types/filters"
  32. "github.com/docker/docker/api/types/network"
  33. "github.com/docker/docker/api/types/swarm"
  34. "github.com/docker/docker/api/types/volume"
  35. "github.com/docker/docker/client"
  36. "github.com/jonboulle/clockwork"
  37. "github.com/sirupsen/logrus"
  38. "github.com/docker/compose/v2/pkg/api"
  39. )
  40. var stdioToStdout bool
  41. func init() {
  42. out, ok := os.LookupEnv("COMPOSE_STATUS_STDOUT")
  43. if ok {
  44. stdioToStdout, _ = strconv.ParseBool(out)
  45. }
  46. }
  47. type Option func(service *composeService) error
  48. // NewComposeService creates a Compose service using Docker CLI.
  49. // This is the standard constructor that requires command.Cli for full functionality.
  50. //
  51. // Example usage:
  52. //
  53. // dockerCli, _ := command.NewDockerCli()
  54. // service := NewComposeService(dockerCli)
  55. //
  56. // For advanced configuration with custom overrides, use ServiceOption functions:
  57. //
  58. // service := NewComposeService(dockerCli,
  59. // WithPrompt(prompt.NewPrompt(cli.In(), cli.Out()).Confirm),
  60. // WithOutputStream(customOut),
  61. // WithErrorStream(customErr),
  62. // WithInputStream(customIn))
  63. //
  64. // Or set all streams at once:
  65. //
  66. // service := NewComposeService(dockerCli,
  67. // WithStreams(customOut, customErr, customIn))
  68. func NewComposeService(dockerCli command.Cli, options ...Option) (api.Compose, error) {
  69. s := &composeService{
  70. dockerCli: dockerCli,
  71. clock: clockwork.NewRealClock(),
  72. maxConcurrency: -1,
  73. dryRun: false,
  74. }
  75. for _, option := range options {
  76. if err := option(s); err != nil {
  77. return nil, err
  78. }
  79. }
  80. if s.prompt == nil {
  81. s.prompt = func(message string, defaultValue bool) (bool, error) {
  82. fmt.Println(message)
  83. logrus.Warning("Compose is running without a 'prompt' component to interact with user")
  84. return defaultValue, nil
  85. }
  86. }
  87. if s.events == nil {
  88. s.events = progress.NewQuiedWriter()
  89. }
  90. // If custom streams were provided, wrap the Docker CLI to use them
  91. if s.outStream != nil || s.errStream != nil || s.inStream != nil {
  92. s.dockerCli = s.wrapDockerCliWithStreams(dockerCli)
  93. }
  94. return s, nil
  95. }
  96. // WithStreams sets custom I/O streams for output and interaction
  97. func WithStreams(out, err io.Writer, in io.Reader) Option {
  98. return func(s *composeService) error {
  99. s.outStream = out
  100. s.errStream = err
  101. s.inStream = in
  102. return nil
  103. }
  104. }
  105. // WithOutputStream sets a custom output stream
  106. func WithOutputStream(out io.Writer) Option {
  107. return func(s *composeService) error {
  108. s.outStream = out
  109. return nil
  110. }
  111. }
  112. // WithErrorStream sets a custom error stream
  113. func WithErrorStream(err io.Writer) Option {
  114. return func(s *composeService) error {
  115. s.errStream = err
  116. return nil
  117. }
  118. }
  119. // WithInputStream sets a custom input stream
  120. func WithInputStream(in io.Reader) Option {
  121. return func(s *composeService) error {
  122. s.inStream = in
  123. return nil
  124. }
  125. }
  126. // WithContextInfo sets custom Docker context information
  127. func WithContextInfo(info api.ContextInfo) Option {
  128. return func(s *composeService) error {
  129. s.contextInfo = info
  130. return nil
  131. }
  132. }
  133. // WithProxyConfig sets custom HTTP proxy configuration for builds
  134. func WithProxyConfig(config map[string]string) Option {
  135. return func(s *composeService) error {
  136. s.proxyConfig = config
  137. return nil
  138. }
  139. }
  140. // WithPrompt configure a UI component for Compose service to interact with user and confirm actions
  141. func WithPrompt(prompt Prompt) Option {
  142. return func(s *composeService) error {
  143. s.prompt = prompt
  144. return nil
  145. }
  146. }
  147. // WithMaxConcurrency defines upper limit for concurrent operations against engine API
  148. func WithMaxConcurrency(maxConcurrency int) Option {
  149. return func(s *composeService) error {
  150. s.maxConcurrency = maxConcurrency
  151. return nil
  152. }
  153. }
  154. // WithDryRun configure Compose to run without actually applying changes
  155. func WithDryRun(s *composeService) error {
  156. s.dryRun = true
  157. cli, err := command.NewDockerCli()
  158. if err != nil {
  159. return err
  160. }
  161. options := flags.NewClientOptions()
  162. options.Context = s.dockerCli.CurrentContext()
  163. err = cli.Initialize(options, command.WithInitializeClient(func(cli *command.DockerCli) (client.APIClient, error) {
  164. return api.NewDryRunClient(s.apiClient(), s.dockerCli)
  165. }))
  166. if err != nil {
  167. return err
  168. }
  169. s.dockerCli = cli
  170. return nil
  171. }
  172. type Prompt func(message string, defaultValue bool) (bool, error)
  173. // WithEventProcessor configure component to get notified on Compose operation and progress events.
  174. // Typically used to configure a progress UI
  175. func WithEventProcessor(bus progress.EventProcessor) Option {
  176. return func(s *composeService) error {
  177. s.events = bus
  178. return nil
  179. }
  180. }
  181. type composeService struct {
  182. dockerCli command.Cli
  183. // prompt is used to interact with user and confirm actions
  184. prompt Prompt
  185. // eventBus collects tasks execution events
  186. events progress.EventProcessor
  187. // Optional overrides for specific components (for SDK users)
  188. outStream io.Writer
  189. errStream io.Writer
  190. inStream io.Reader
  191. contextInfo api.ContextInfo
  192. proxyConfig map[string]string
  193. clock clockwork.Clock
  194. maxConcurrency int
  195. dryRun bool
  196. }
  197. // Close releases any connections/resources held by the underlying clients.
  198. //
  199. // In practice, this service has the same lifetime as the process, so everything
  200. // will get cleaned up at about the same time regardless even if not invoked.
  201. func (s *composeService) Close() error {
  202. var errs []error
  203. if s.dockerCli != nil {
  204. errs = append(errs, s.apiClient().Close())
  205. }
  206. return errors.Join(errs...)
  207. }
  208. func (s *composeService) apiClient() client.APIClient {
  209. return s.dockerCli.Client()
  210. }
  211. func (s *composeService) configFile() *configfile.ConfigFile {
  212. return s.dockerCli.ConfigFile()
  213. }
  214. // getContextInfo returns the context info - either custom override or dockerCli adapter
  215. func (s *composeService) getContextInfo() api.ContextInfo {
  216. if s.contextInfo != nil {
  217. return s.contextInfo
  218. }
  219. return &dockerCliContextInfo{cli: s.dockerCli}
  220. }
  221. // getProxyConfig returns the proxy config - either custom override or environment-based
  222. func (s *composeService) getProxyConfig() map[string]string {
  223. if s.proxyConfig != nil {
  224. return s.proxyConfig
  225. }
  226. return storeutil.GetProxyConfig(s.dockerCli)
  227. }
  228. func (s *composeService) stdout() *streams.Out {
  229. return s.dockerCli.Out()
  230. }
  231. func (s *composeService) stdin() *streams.In {
  232. return s.dockerCli.In()
  233. }
  234. func (s *composeService) stderr() *streams.Out {
  235. return s.dockerCli.Err()
  236. }
  237. func (s *composeService) stdinfo() *streams.Out {
  238. if stdioToStdout {
  239. return s.stdout()
  240. }
  241. return s.stderr()
  242. }
  243. // GetConfiguredStreams returns the configured I/O streams (implements api.Compose interface)
  244. func (s *composeService) GetConfiguredStreams() (io.Writer, io.Writer, io.Reader) {
  245. return s.stdout(), s.stderr(), s.stdin()
  246. }
  247. // readCloserAdapter adapts io.Reader to io.ReadCloser
  248. type readCloserAdapter struct {
  249. r io.Reader
  250. }
  251. func (r *readCloserAdapter) Read(p []byte) (int, error) {
  252. return r.r.Read(p)
  253. }
  254. func (r *readCloserAdapter) Close() error {
  255. return nil
  256. }
  257. // wrapDockerCliWithStreams wraps the Docker CLI to intercept and override stream methods
  258. func (s *composeService) wrapDockerCliWithStreams(baseCli command.Cli) command.Cli {
  259. wrapper := &streamOverrideWrapper{
  260. Cli: baseCli,
  261. }
  262. // Wrap custom streams in Docker CLI's stream types
  263. if s.outStream != nil {
  264. wrapper.outStream = streams.NewOut(s.outStream)
  265. }
  266. if s.errStream != nil {
  267. wrapper.errStream = streams.NewOut(s.errStream)
  268. }
  269. if s.inStream != nil {
  270. wrapper.inStream = streams.NewIn(&readCloserAdapter{r: s.inStream})
  271. }
  272. return wrapper
  273. }
  274. // streamOverrideWrapper wraps command.Cli to override streams with custom implementations
  275. type streamOverrideWrapper struct {
  276. command.Cli
  277. outStream *streams.Out
  278. errStream *streams.Out
  279. inStream *streams.In
  280. }
  281. func (w *streamOverrideWrapper) Out() *streams.Out {
  282. if w.outStream != nil {
  283. return w.outStream
  284. }
  285. return w.Cli.Out()
  286. }
  287. func (w *streamOverrideWrapper) Err() *streams.Out {
  288. if w.errStream != nil {
  289. return w.errStream
  290. }
  291. return w.Cli.Err()
  292. }
  293. func (w *streamOverrideWrapper) In() *streams.In {
  294. if w.inStream != nil {
  295. return w.inStream
  296. }
  297. return w.Cli.In()
  298. }
  299. func getCanonicalContainerName(c container.Summary) string {
  300. if len(c.Names) == 0 {
  301. // corner case, sometime happens on removal. return short ID as a safeguard value
  302. return c.ID[:12]
  303. }
  304. // Names return container canonical name /foo + link aliases /linked_by/foo
  305. for _, name := range c.Names {
  306. if strings.LastIndex(name, "/") == 0 {
  307. return name[1:]
  308. }
  309. }
  310. return strings.TrimPrefix(c.Names[0], "/")
  311. }
  312. func getContainerNameWithoutProject(c container.Summary) string {
  313. project := c.Labels[api.ProjectLabel]
  314. defaultName := getDefaultContainerName(project, c.Labels[api.ServiceLabel], c.Labels[api.ContainerNumberLabel])
  315. name := getCanonicalContainerName(c)
  316. if name != defaultName {
  317. // service declares a custom container_name
  318. return name
  319. }
  320. return name[len(project)+1:]
  321. }
  322. // projectFromName builds a types.Project based on actual resources with compose labels set
  323. func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) {
  324. project := &types.Project{
  325. Name: projectName,
  326. Services: types.Services{},
  327. }
  328. if len(containers) == 0 {
  329. return project, fmt.Errorf("no container found for project %q: %w", projectName, api.ErrNotFound)
  330. }
  331. set := types.Services{}
  332. for _, c := range containers {
  333. serviceLabel, ok := c.Labels[api.ServiceLabel]
  334. if !ok {
  335. serviceLabel = getCanonicalContainerName(c)
  336. }
  337. service, ok := set[serviceLabel]
  338. if !ok {
  339. service = types.ServiceConfig{
  340. Name: serviceLabel,
  341. Image: c.Image,
  342. Labels: c.Labels,
  343. }
  344. }
  345. service.Scale = increment(service.Scale)
  346. set[serviceLabel] = service
  347. }
  348. for name, service := range set {
  349. dependencies := service.Labels[api.DependenciesLabel]
  350. if dependencies != "" {
  351. service.DependsOn = types.DependsOnConfig{}
  352. for _, dc := range strings.Split(dependencies, ",") {
  353. dcArr := strings.Split(dc, ":")
  354. condition := ServiceConditionRunningOrHealthy
  355. // Let's restart the dependency by default if we don't have the info stored in the label
  356. restart := true
  357. required := true
  358. dependency := dcArr[0]
  359. // backward compatibility
  360. if len(dcArr) > 1 {
  361. condition = dcArr[1]
  362. if len(dcArr) > 2 {
  363. restart, _ = strconv.ParseBool(dcArr[2])
  364. }
  365. }
  366. service.DependsOn[dependency] = types.ServiceDependency{Condition: condition, Restart: restart, Required: required}
  367. }
  368. set[name] = service
  369. }
  370. }
  371. project.Services = set
  372. SERVICES:
  373. for _, qs := range services {
  374. for _, es := range project.Services {
  375. if es.Name == qs {
  376. continue SERVICES
  377. }
  378. }
  379. return project, fmt.Errorf("no such service: %q: %w", qs, api.ErrNotFound)
  380. }
  381. project, err := project.WithSelectedServices(services)
  382. if err != nil {
  383. return project, err
  384. }
  385. return project, nil
  386. }
  387. func increment(scale *int) *int {
  388. i := 1
  389. if scale != nil {
  390. i = *scale + 1
  391. }
  392. return &i
  393. }
  394. func (s *composeService) actualVolumes(ctx context.Context, projectName string) (types.Volumes, error) {
  395. opts := volume.ListOptions{
  396. Filters: filters.NewArgs(projectFilter(projectName)),
  397. }
  398. volumes, err := s.apiClient().VolumeList(ctx, opts)
  399. if err != nil {
  400. return nil, err
  401. }
  402. actual := types.Volumes{}
  403. for _, vol := range volumes.Volumes {
  404. actual[vol.Labels[api.VolumeLabel]] = types.VolumeConfig{
  405. Name: vol.Name,
  406. Driver: vol.Driver,
  407. Labels: vol.Labels,
  408. }
  409. }
  410. return actual, nil
  411. }
  412. func (s *composeService) actualNetworks(ctx context.Context, projectName string) (types.Networks, error) {
  413. networks, err := s.apiClient().NetworkList(ctx, network.ListOptions{
  414. Filters: filters.NewArgs(projectFilter(projectName)),
  415. })
  416. if err != nil {
  417. return nil, err
  418. }
  419. actual := types.Networks{}
  420. for _, net := range networks {
  421. actual[net.Labels[api.NetworkLabel]] = types.NetworkConfig{
  422. Name: net.Name,
  423. Driver: net.Driver,
  424. Labels: net.Labels,
  425. }
  426. }
  427. return actual, nil
  428. }
  429. var swarmEnabled = struct {
  430. once sync.Once
  431. val bool
  432. err error
  433. }{}
  434. func (s *composeService) isSWarmEnabled(ctx context.Context) (bool, error) {
  435. swarmEnabled.once.Do(func() {
  436. info, err := s.apiClient().Info(ctx)
  437. if err != nil {
  438. swarmEnabled.err = err
  439. }
  440. switch info.Swarm.LocalNodeState {
  441. case swarm.LocalNodeStateInactive, swarm.LocalNodeStateLocked:
  442. swarmEnabled.val = false
  443. default:
  444. swarmEnabled.val = true
  445. }
  446. })
  447. return swarmEnabled.val, swarmEnabled.err
  448. }
  449. type runtimeVersionCache struct {
  450. once sync.Once
  451. val string
  452. err error
  453. }
  454. var runtimeVersion runtimeVersionCache
  455. func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
  456. runtimeVersion.once.Do(func() {
  457. version, err := s.apiClient().ServerVersion(ctx)
  458. if err != nil {
  459. runtimeVersion.err = err
  460. }
  461. runtimeVersion.val = version.APIVersion
  462. })
  463. return runtimeVersion.val, runtimeVersion.err
  464. }