compose.go 13 KB

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