compose.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  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. // readCloserAdapter adapts io.Reader to io.ReadCloser
  250. type readCloserAdapter struct {
  251. r io.Reader
  252. }
  253. func (r *readCloserAdapter) Read(p []byte) (int, error) {
  254. return r.r.Read(p)
  255. }
  256. func (r *readCloserAdapter) Close() error {
  257. return nil
  258. }
  259. // wrapDockerCliWithStreams wraps the Docker CLI to intercept and override stream methods
  260. func (s *composeService) wrapDockerCliWithStreams(baseCli command.Cli) command.Cli {
  261. wrapper := &streamOverrideWrapper{
  262. Cli: baseCli,
  263. }
  264. // Wrap custom streams in Docker CLI's stream types
  265. if s.outStream != nil {
  266. wrapper.outStream = streams.NewOut(s.outStream)
  267. }
  268. if s.errStream != nil {
  269. wrapper.errStream = streams.NewOut(s.errStream)
  270. }
  271. if s.inStream != nil {
  272. wrapper.inStream = streams.NewIn(&readCloserAdapter{r: s.inStream})
  273. }
  274. return wrapper
  275. }
  276. // streamOverrideWrapper wraps command.Cli to override streams with custom implementations
  277. type streamOverrideWrapper struct {
  278. command.Cli
  279. outStream *streams.Out
  280. errStream *streams.Out
  281. inStream *streams.In
  282. }
  283. func (w *streamOverrideWrapper) Out() *streams.Out {
  284. if w.outStream != nil {
  285. return w.outStream
  286. }
  287. return w.Cli.Out()
  288. }
  289. func (w *streamOverrideWrapper) Err() *streams.Out {
  290. if w.errStream != nil {
  291. return w.errStream
  292. }
  293. return w.Cli.Err()
  294. }
  295. func (w *streamOverrideWrapper) In() *streams.In {
  296. if w.inStream != nil {
  297. return w.inStream
  298. }
  299. return w.Cli.In()
  300. }
  301. func getCanonicalContainerName(c container.Summary) string {
  302. if len(c.Names) == 0 {
  303. // corner case, sometime happens on removal. return short ID as a safeguard value
  304. return c.ID[:12]
  305. }
  306. // Names return container canonical name /foo + link aliases /linked_by/foo
  307. for _, name := range c.Names {
  308. if strings.LastIndex(name, "/") == 0 {
  309. return name[1:]
  310. }
  311. }
  312. return strings.TrimPrefix(c.Names[0], "/")
  313. }
  314. func getContainerNameWithoutProject(c container.Summary) string {
  315. project := c.Labels[api.ProjectLabel]
  316. defaultName := getDefaultContainerName(project, c.Labels[api.ServiceLabel], c.Labels[api.ContainerNumberLabel])
  317. name := getCanonicalContainerName(c)
  318. if name != defaultName {
  319. // service declares a custom container_name
  320. return name
  321. }
  322. return name[len(project)+1:]
  323. }
  324. // projectFromName builds a types.Project based on actual resources with compose labels set
  325. func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) {
  326. project := &types.Project{
  327. Name: projectName,
  328. Services: types.Services{},
  329. }
  330. if len(containers) == 0 {
  331. return project, fmt.Errorf("no container found for project %q: %w", projectName, api.ErrNotFound)
  332. }
  333. set := types.Services{}
  334. for _, c := range containers {
  335. serviceLabel, ok := c.Labels[api.ServiceLabel]
  336. if !ok {
  337. serviceLabel = getCanonicalContainerName(c)
  338. }
  339. service, ok := set[serviceLabel]
  340. if !ok {
  341. service = types.ServiceConfig{
  342. Name: serviceLabel,
  343. Image: c.Image,
  344. Labels: c.Labels,
  345. }
  346. }
  347. service.Scale = increment(service.Scale)
  348. set[serviceLabel] = service
  349. }
  350. for name, service := range set {
  351. dependencies := service.Labels[api.DependenciesLabel]
  352. if dependencies != "" {
  353. service.DependsOn = types.DependsOnConfig{}
  354. for _, dc := range strings.Split(dependencies, ",") {
  355. dcArr := strings.Split(dc, ":")
  356. condition := ServiceConditionRunningOrHealthy
  357. // Let's restart the dependency by default if we don't have the info stored in the label
  358. restart := true
  359. required := true
  360. dependency := dcArr[0]
  361. // backward compatibility
  362. if len(dcArr) > 1 {
  363. condition = dcArr[1]
  364. if len(dcArr) > 2 {
  365. restart, _ = strconv.ParseBool(dcArr[2])
  366. }
  367. }
  368. service.DependsOn[dependency] = types.ServiceDependency{Condition: condition, Restart: restart, Required: required}
  369. }
  370. set[name] = service
  371. }
  372. }
  373. project.Services = set
  374. SERVICES:
  375. for _, qs := range services {
  376. for _, es := range project.Services {
  377. if es.Name == qs {
  378. continue SERVICES
  379. }
  380. }
  381. return project, fmt.Errorf("no such service: %q: %w", qs, api.ErrNotFound)
  382. }
  383. project, err := project.WithSelectedServices(services)
  384. if err != nil {
  385. return project, err
  386. }
  387. return project, nil
  388. }
  389. func increment(scale *int) *int {
  390. i := 1
  391. if scale != nil {
  392. i = *scale + 1
  393. }
  394. return &i
  395. }
  396. func (s *composeService) actualVolumes(ctx context.Context, projectName string) (types.Volumes, error) {
  397. opts := volume.ListOptions{
  398. Filters: filters.NewArgs(projectFilter(projectName)),
  399. }
  400. volumes, err := s.apiClient().VolumeList(ctx, opts)
  401. if err != nil {
  402. return nil, err
  403. }
  404. actual := types.Volumes{}
  405. for _, vol := range volumes.Volumes {
  406. actual[vol.Labels[api.VolumeLabel]] = types.VolumeConfig{
  407. Name: vol.Name,
  408. Driver: vol.Driver,
  409. Labels: vol.Labels,
  410. }
  411. }
  412. return actual, nil
  413. }
  414. func (s *composeService) actualNetworks(ctx context.Context, projectName string) (types.Networks, error) {
  415. networks, err := s.apiClient().NetworkList(ctx, network.ListOptions{
  416. Filters: filters.NewArgs(projectFilter(projectName)),
  417. })
  418. if err != nil {
  419. return nil, err
  420. }
  421. actual := types.Networks{}
  422. for _, net := range networks {
  423. actual[net.Labels[api.NetworkLabel]] = types.NetworkConfig{
  424. Name: net.Name,
  425. Driver: net.Driver,
  426. Labels: net.Labels,
  427. }
  428. }
  429. return actual, nil
  430. }
  431. var swarmEnabled = struct {
  432. once sync.Once
  433. val bool
  434. err error
  435. }{}
  436. func (s *composeService) isSWarmEnabled(ctx context.Context) (bool, error) {
  437. swarmEnabled.once.Do(func() {
  438. info, err := s.apiClient().Info(ctx)
  439. if err != nil {
  440. swarmEnabled.err = err
  441. }
  442. switch info.Swarm.LocalNodeState {
  443. case swarm.LocalNodeStateInactive, swarm.LocalNodeStateLocked:
  444. swarmEnabled.val = false
  445. default:
  446. swarmEnabled.val = true
  447. }
  448. })
  449. return swarmEnabled.val, swarmEnabled.err
  450. }
  451. type runtimeVersionCache struct {
  452. once sync.Once
  453. val string
  454. err error
  455. }
  456. var runtimeVersion runtimeVersionCache
  457. func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
  458. runtimeVersion.once.Do(func() {
  459. version, err := s.apiClient().ServerVersion(ctx)
  460. if err != nil {
  461. runtimeVersion.err = err
  462. }
  463. runtimeVersion.val = version.APIVersion
  464. })
  465. return runtimeVersion.val, runtimeVersion.err
  466. }