compose.go 13 KB

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