compose.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  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.NewQuietWriter()
  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. // AlwaysOkPrompt returns a Prompt implementation that always returns true without user interaction.
  174. func AlwaysOkPrompt() Prompt {
  175. return func(message string, defaultValue bool) (bool, error) {
  176. return true, nil
  177. }
  178. }
  179. // WithEventProcessor configure component to get notified on Compose operation and progress events.
  180. // Typically used to configure a progress UI
  181. func WithEventProcessor(bus progress.EventProcessor) Option {
  182. return func(s *composeService) error {
  183. s.events = bus
  184. return nil
  185. }
  186. }
  187. type composeService struct {
  188. dockerCli command.Cli
  189. // prompt is used to interact with user and confirm actions
  190. prompt Prompt
  191. // eventBus collects tasks execution events
  192. events progress.EventProcessor
  193. // Optional overrides for specific components (for SDK users)
  194. outStream io.Writer
  195. errStream io.Writer
  196. inStream io.Reader
  197. contextInfo api.ContextInfo
  198. proxyConfig map[string]string
  199. clock clockwork.Clock
  200. maxConcurrency int
  201. dryRun bool
  202. }
  203. // Close releases any connections/resources held by the underlying clients.
  204. //
  205. // In practice, this service has the same lifetime as the process, so everything
  206. // will get cleaned up at about the same time regardless even if not invoked.
  207. func (s *composeService) Close() error {
  208. var errs []error
  209. if s.dockerCli != nil {
  210. errs = append(errs, s.apiClient().Close())
  211. }
  212. return errors.Join(errs...)
  213. }
  214. func (s *composeService) apiClient() client.APIClient {
  215. return s.dockerCli.Client()
  216. }
  217. func (s *composeService) configFile() *configfile.ConfigFile {
  218. return s.dockerCli.ConfigFile()
  219. }
  220. // getContextInfo returns the context info - either custom override or dockerCli adapter
  221. func (s *composeService) getContextInfo() api.ContextInfo {
  222. if s.contextInfo != nil {
  223. return s.contextInfo
  224. }
  225. return &dockerCliContextInfo{cli: s.dockerCli}
  226. }
  227. // getProxyConfig returns the proxy config - either custom override or environment-based
  228. func (s *composeService) getProxyConfig() map[string]string {
  229. if s.proxyConfig != nil {
  230. return s.proxyConfig
  231. }
  232. return storeutil.GetProxyConfig(s.dockerCli)
  233. }
  234. func (s *composeService) stdout() *streams.Out {
  235. return s.dockerCli.Out()
  236. }
  237. func (s *composeService) stdin() *streams.In {
  238. return s.dockerCli.In()
  239. }
  240. func (s *composeService) stderr() *streams.Out {
  241. return s.dockerCli.Err()
  242. }
  243. func (s *composeService) stdinfo() *streams.Out {
  244. if stdioToStdout {
  245. return s.stdout()
  246. }
  247. return s.stderr()
  248. }
  249. // GetConfiguredStreams returns the configured I/O streams (implements api.Compose interface)
  250. func (s *composeService) GetConfiguredStreams() (io.Writer, io.Writer, io.Reader) {
  251. return s.stdout(), s.stderr(), s.stdin()
  252. }
  253. // readCloserAdapter adapts io.Reader to io.ReadCloser
  254. type readCloserAdapter struct {
  255. r io.Reader
  256. }
  257. func (r *readCloserAdapter) Read(p []byte) (int, error) {
  258. return r.r.Read(p)
  259. }
  260. func (r *readCloserAdapter) Close() error {
  261. return nil
  262. }
  263. // wrapDockerCliWithStreams wraps the Docker CLI to intercept and override stream methods
  264. func (s *composeService) wrapDockerCliWithStreams(baseCli command.Cli) command.Cli {
  265. wrapper := &streamOverrideWrapper{
  266. Cli: baseCli,
  267. }
  268. // Wrap custom streams in Docker CLI's stream types
  269. if s.outStream != nil {
  270. wrapper.outStream = streams.NewOut(s.outStream)
  271. }
  272. if s.errStream != nil {
  273. wrapper.errStream = streams.NewOut(s.errStream)
  274. }
  275. if s.inStream != nil {
  276. wrapper.inStream = streams.NewIn(&readCloserAdapter{r: s.inStream})
  277. }
  278. return wrapper
  279. }
  280. // streamOverrideWrapper wraps command.Cli to override streams with custom implementations
  281. type streamOverrideWrapper struct {
  282. command.Cli
  283. outStream *streams.Out
  284. errStream *streams.Out
  285. inStream *streams.In
  286. }
  287. func (w *streamOverrideWrapper) Out() *streams.Out {
  288. if w.outStream != nil {
  289. return w.outStream
  290. }
  291. return w.Cli.Out()
  292. }
  293. func (w *streamOverrideWrapper) Err() *streams.Out {
  294. if w.errStream != nil {
  295. return w.errStream
  296. }
  297. return w.Cli.Err()
  298. }
  299. func (w *streamOverrideWrapper) In() *streams.In {
  300. if w.inStream != nil {
  301. return w.inStream
  302. }
  303. return w.Cli.In()
  304. }
  305. func getCanonicalContainerName(c container.Summary) string {
  306. if len(c.Names) == 0 {
  307. // corner case, sometime happens on removal. return short ID as a safeguard value
  308. return c.ID[:12]
  309. }
  310. // Names return container canonical name /foo + link aliases /linked_by/foo
  311. for _, name := range c.Names {
  312. if strings.LastIndex(name, "/") == 0 {
  313. return name[1:]
  314. }
  315. }
  316. return strings.TrimPrefix(c.Names[0], "/")
  317. }
  318. func getContainerNameWithoutProject(c container.Summary) string {
  319. project := c.Labels[api.ProjectLabel]
  320. defaultName := getDefaultContainerName(project, c.Labels[api.ServiceLabel], c.Labels[api.ContainerNumberLabel])
  321. name := getCanonicalContainerName(c)
  322. if name != defaultName {
  323. // service declares a custom container_name
  324. return name
  325. }
  326. return name[len(project)+1:]
  327. }
  328. // projectFromName builds a types.Project based on actual resources with compose labels set
  329. func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) {
  330. project := &types.Project{
  331. Name: projectName,
  332. Services: types.Services{},
  333. }
  334. if len(containers) == 0 {
  335. return project, fmt.Errorf("no container found for project %q: %w", projectName, api.ErrNotFound)
  336. }
  337. set := types.Services{}
  338. for _, c := range containers {
  339. serviceLabel, ok := c.Labels[api.ServiceLabel]
  340. if !ok {
  341. serviceLabel = getCanonicalContainerName(c)
  342. }
  343. service, ok := set[serviceLabel]
  344. if !ok {
  345. service = types.ServiceConfig{
  346. Name: serviceLabel,
  347. Image: c.Image,
  348. Labels: c.Labels,
  349. }
  350. }
  351. service.Scale = increment(service.Scale)
  352. set[serviceLabel] = service
  353. }
  354. for name, service := range set {
  355. dependencies := service.Labels[api.DependenciesLabel]
  356. if dependencies != "" {
  357. service.DependsOn = types.DependsOnConfig{}
  358. for _, dc := range strings.Split(dependencies, ",") {
  359. dcArr := strings.Split(dc, ":")
  360. condition := ServiceConditionRunningOrHealthy
  361. // Let's restart the dependency by default if we don't have the info stored in the label
  362. restart := true
  363. required := true
  364. dependency := dcArr[0]
  365. // backward compatibility
  366. if len(dcArr) > 1 {
  367. condition = dcArr[1]
  368. if len(dcArr) > 2 {
  369. restart, _ = strconv.ParseBool(dcArr[2])
  370. }
  371. }
  372. service.DependsOn[dependency] = types.ServiceDependency{Condition: condition, Restart: restart, Required: required}
  373. }
  374. set[name] = service
  375. }
  376. }
  377. project.Services = set
  378. SERVICES:
  379. for _, qs := range services {
  380. for _, es := range project.Services {
  381. if es.Name == qs {
  382. continue SERVICES
  383. }
  384. }
  385. return project, fmt.Errorf("no such service: %q: %w", qs, api.ErrNotFound)
  386. }
  387. project, err := project.WithSelectedServices(services)
  388. if err != nil {
  389. return project, err
  390. }
  391. return project, nil
  392. }
  393. func increment(scale *int) *int {
  394. i := 1
  395. if scale != nil {
  396. i = *scale + 1
  397. }
  398. return &i
  399. }
  400. func (s *composeService) actualVolumes(ctx context.Context, projectName string) (types.Volumes, error) {
  401. opts := volume.ListOptions{
  402. Filters: filters.NewArgs(projectFilter(projectName)),
  403. }
  404. volumes, err := s.apiClient().VolumeList(ctx, opts)
  405. if err != nil {
  406. return nil, err
  407. }
  408. actual := types.Volumes{}
  409. for _, vol := range volumes.Volumes {
  410. actual[vol.Labels[api.VolumeLabel]] = types.VolumeConfig{
  411. Name: vol.Name,
  412. Driver: vol.Driver,
  413. Labels: vol.Labels,
  414. }
  415. }
  416. return actual, nil
  417. }
  418. func (s *composeService) actualNetworks(ctx context.Context, projectName string) (types.Networks, error) {
  419. networks, err := s.apiClient().NetworkList(ctx, network.ListOptions{
  420. Filters: filters.NewArgs(projectFilter(projectName)),
  421. })
  422. if err != nil {
  423. return nil, err
  424. }
  425. actual := types.Networks{}
  426. for _, net := range networks {
  427. actual[net.Labels[api.NetworkLabel]] = types.NetworkConfig{
  428. Name: net.Name,
  429. Driver: net.Driver,
  430. Labels: net.Labels,
  431. }
  432. }
  433. return actual, nil
  434. }
  435. var swarmEnabled = struct {
  436. once sync.Once
  437. val bool
  438. err error
  439. }{}
  440. func (s *composeService) isSWarmEnabled(ctx context.Context) (bool, error) {
  441. swarmEnabled.once.Do(func() {
  442. info, err := s.apiClient().Info(ctx)
  443. if err != nil {
  444. swarmEnabled.err = err
  445. }
  446. switch info.Swarm.LocalNodeState {
  447. case swarm.LocalNodeStateInactive, swarm.LocalNodeStateLocked:
  448. swarmEnabled.val = false
  449. default:
  450. swarmEnabled.val = true
  451. }
  452. })
  453. return swarmEnabled.val, swarmEnabled.err
  454. }
  455. type runtimeVersionCache struct {
  456. once sync.Once
  457. val string
  458. err error
  459. }
  460. var runtimeVersion runtimeVersionCache
  461. func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
  462. runtimeVersion.once.Do(func() {
  463. version, err := s.apiClient().ServerVersion(ctx)
  464. if err != nil {
  465. runtimeVersion.err = err
  466. }
  467. runtimeVersion.val = version.APIVersion
  468. })
  469. return runtimeVersion.val, runtimeVersion.err
  470. }