compose.go 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  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 local
  14. import (
  15. "bytes"
  16. "context"
  17. "encoding/base64"
  18. "encoding/json"
  19. "fmt"
  20. "io"
  21. "path/filepath"
  22. "sort"
  23. "strconv"
  24. "strings"
  25. "github.com/compose-spec/compose-go/cli"
  26. "github.com/compose-spec/compose-go/types"
  27. "github.com/docker/buildx/build"
  28. cliconfig "github.com/docker/cli/cli/config"
  29. "github.com/docker/distribution/reference"
  30. moby "github.com/docker/docker/api/types"
  31. "github.com/docker/docker/api/types/container"
  32. "github.com/docker/docker/api/types/filters"
  33. "github.com/docker/docker/api/types/mount"
  34. "github.com/docker/docker/api/types/network"
  35. "github.com/docker/docker/api/types/strslice"
  36. mobyvolume "github.com/docker/docker/api/types/volume"
  37. "github.com/docker/docker/client"
  38. "github.com/docker/docker/errdefs"
  39. "github.com/docker/docker/pkg/jsonmessage"
  40. "github.com/docker/docker/pkg/stdcopy"
  41. "github.com/docker/docker/registry"
  42. "github.com/docker/go-connections/nat"
  43. "github.com/pkg/errors"
  44. "github.com/sanathkr/go-yaml"
  45. "golang.org/x/sync/errgroup"
  46. "github.com/docker/compose-cli/api/compose"
  47. "github.com/docker/compose-cli/config"
  48. errdefs2 "github.com/docker/compose-cli/errdefs"
  49. "github.com/docker/compose-cli/progress"
  50. )
  51. type composeService struct {
  52. apiClient *client.Client
  53. }
  54. func (s *composeService) Build(ctx context.Context, project *types.Project) error {
  55. opts := map[string]build.Options{}
  56. for _, service := range project.Services {
  57. if service.Build != nil {
  58. imageName := service.Image
  59. if imageName == "" {
  60. imageName = project.Name + "_" + service.Name
  61. }
  62. opts[imageName] = s.toBuildOptions(service, project.WorkingDir, imageName)
  63. }
  64. }
  65. return s.build(ctx, project, opts)
  66. }
  67. func (s *composeService) Push(ctx context.Context, project *types.Project) error {
  68. configFile, err := cliconfig.Load(config.Dir(ctx))
  69. if err != nil {
  70. return err
  71. }
  72. eg, ctx := errgroup.WithContext(ctx)
  73. info, err := s.apiClient.Info(ctx)
  74. if err != nil {
  75. return err
  76. }
  77. if info.IndexServerAddress == "" {
  78. info.IndexServerAddress = registry.IndexServer
  79. }
  80. for _, service := range project.Services {
  81. if service.Build == nil {
  82. continue
  83. }
  84. service := service
  85. eg.Go(func() error {
  86. w := progress.ContextWriter(ctx)
  87. ref, err := reference.ParseNormalizedNamed(service.Image)
  88. if err != nil {
  89. return err
  90. }
  91. repoInfo, err := registry.ParseRepositoryInfo(ref)
  92. if err != nil {
  93. return err
  94. }
  95. key := repoInfo.Index.Name
  96. if repoInfo.Index.Official {
  97. key = info.IndexServerAddress
  98. }
  99. authConfig, err := configFile.GetAuthConfig(key)
  100. if err != nil {
  101. return err
  102. }
  103. buf, err := json.Marshal(authConfig)
  104. if err != nil {
  105. return err
  106. }
  107. stream, err := s.apiClient.ImagePush(ctx, service.Image, moby.ImagePushOptions{
  108. RegistryAuth: base64.URLEncoding.EncodeToString(buf),
  109. })
  110. if err != nil {
  111. return err
  112. }
  113. dec := json.NewDecoder(stream)
  114. for {
  115. var jm jsonmessage.JSONMessage
  116. if err := dec.Decode(&jm); err != nil {
  117. if err == io.EOF {
  118. break
  119. }
  120. return err
  121. }
  122. if jm.Error != nil {
  123. return errors.New(jm.Error.Message)
  124. }
  125. toProgressEvent("Pushing "+service.Name, jm, w)
  126. }
  127. return nil
  128. })
  129. }
  130. return eg.Wait()
  131. }
  132. func (s *composeService) Pull(ctx context.Context, project *types.Project) error {
  133. configFile, err := cliconfig.Load(config.Dir(ctx))
  134. if err != nil {
  135. return err
  136. }
  137. info, err := s.apiClient.Info(ctx)
  138. if err != nil {
  139. return err
  140. }
  141. if info.IndexServerAddress == "" {
  142. info.IndexServerAddress = registry.IndexServer
  143. }
  144. w := progress.ContextWriter(ctx)
  145. eg, ctx := errgroup.WithContext(ctx)
  146. for _, srv := range project.Services {
  147. service := srv
  148. eg.Go(func() error {
  149. w.Event(progress.Event{
  150. ID: service.Name,
  151. Status: progress.Working,
  152. Text: "Pulling",
  153. })
  154. ref, err := reference.ParseNormalizedNamed(service.Image)
  155. if err != nil {
  156. return err
  157. }
  158. repoInfo, err := registry.ParseRepositoryInfo(ref)
  159. if err != nil {
  160. return err
  161. }
  162. key := repoInfo.Index.Name
  163. if repoInfo.Index.Official {
  164. key = info.IndexServerAddress
  165. }
  166. authConfig, err := configFile.GetAuthConfig(key)
  167. if err != nil {
  168. return err
  169. }
  170. buf, err := json.Marshal(authConfig)
  171. if err != nil {
  172. return err
  173. }
  174. stream, err := s.apiClient.ImagePull(ctx, service.Image, moby.ImagePullOptions{
  175. RegistryAuth: base64.URLEncoding.EncodeToString(buf),
  176. })
  177. if err != nil {
  178. w.Event(progress.Event{
  179. ID: service.Name,
  180. Status: progress.Error,
  181. Text: "Error",
  182. })
  183. return err
  184. }
  185. dec := json.NewDecoder(stream)
  186. for {
  187. var jm jsonmessage.JSONMessage
  188. if err := dec.Decode(&jm); err != nil {
  189. if err == io.EOF {
  190. break
  191. }
  192. return err
  193. }
  194. if jm.Error != nil {
  195. return errors.New(jm.Error.Message)
  196. }
  197. toPullProgressEvent(service.Name, jm, w)
  198. }
  199. w.Event(progress.Event{
  200. ID: service.Name,
  201. Status: progress.Done,
  202. Text: "Pulled",
  203. })
  204. return nil
  205. })
  206. }
  207. return eg.Wait()
  208. }
  209. func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, w progress.Writer) {
  210. if jm.ID == "" || jm.Progress == nil {
  211. return
  212. }
  213. var (
  214. text string
  215. status = progress.Working
  216. )
  217. text = jm.Progress.String()
  218. if jm.Status == "Pull complete" ||
  219. jm.Status == "Already exists" ||
  220. strings.Contains(jm.Status, "Image is up to date") ||
  221. strings.Contains(jm.Status, "Downloaded newer image") {
  222. status = progress.Done
  223. }
  224. if jm.Error != nil {
  225. status = progress.Error
  226. text = jm.Error.Message
  227. }
  228. w.Event(progress.Event{
  229. ID: jm.ID,
  230. ParentID: parent,
  231. Text: jm.Status,
  232. Status: status,
  233. StatusText: text,
  234. })
  235. }
  236. func toProgressEvent(prefix string, jm jsonmessage.JSONMessage, w progress.Writer) {
  237. if jm.ID == "" {
  238. // skipped
  239. return
  240. }
  241. var (
  242. text string
  243. status = progress.Working
  244. )
  245. if jm.Status == "Pull complete" || jm.Status == "Already exists" {
  246. status = progress.Done
  247. }
  248. if jm.Error != nil {
  249. status = progress.Error
  250. text = jm.Error.Message
  251. }
  252. if jm.Progress != nil {
  253. text = jm.Progress.String()
  254. }
  255. w.Event(progress.Event{
  256. ID: fmt.Sprintf("Pushing %s: %s", prefix, jm.ID),
  257. Text: jm.Status,
  258. Status: status,
  259. StatusText: text,
  260. })
  261. }
  262. func (s *composeService) Up(ctx context.Context, project *types.Project, detach bool) error {
  263. return errdefs2.ErrNotImplemented
  264. }
  265. func (s *composeService) Create(ctx context.Context, project *types.Project) error {
  266. err := s.ensureImagesExists(ctx, project)
  267. if err != nil {
  268. return err
  269. }
  270. for k, network := range project.Networks {
  271. if !network.External.External && network.Name != "" {
  272. network.Name = fmt.Sprintf("%s_%s", project.Name, k)
  273. project.Networks[k] = network
  274. }
  275. network.Labels = network.Labels.Add(networkLabel, k)
  276. network.Labels = network.Labels.Add(projectLabel, project.Name)
  277. network.Labels = network.Labels.Add(versionLabel, ComposeVersion)
  278. err := s.ensureNetwork(ctx, network)
  279. if err != nil {
  280. return err
  281. }
  282. }
  283. for k, volume := range project.Volumes {
  284. if !volume.External.External && volume.Name != "" {
  285. volume.Name = fmt.Sprintf("%s_%s", project.Name, k)
  286. project.Volumes[k] = volume
  287. }
  288. volume.Labels = volume.Labels.Add(volumeLabel, k)
  289. volume.Labels = volume.Labels.Add(projectLabel, project.Name)
  290. volume.Labels = volume.Labels.Add(versionLabel, ComposeVersion)
  291. err := s.ensureVolume(ctx, volume)
  292. if err != nil {
  293. return err
  294. }
  295. }
  296. return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
  297. return s.ensureService(c, project, service)
  298. })
  299. }
  300. func (s *composeService) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
  301. var group *errgroup.Group
  302. if consumer != nil {
  303. eg, err := s.attach(ctx, project, consumer)
  304. if err != nil {
  305. return err
  306. }
  307. group = eg
  308. }
  309. err := InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
  310. return s.startService(ctx, project, service)
  311. })
  312. if err != nil {
  313. return err
  314. }
  315. if group != nil {
  316. return group.Wait()
  317. }
  318. return nil
  319. }
  320. func (s *composeService) attach(ctx context.Context, project *types.Project, consumer compose.LogConsumer) (*errgroup.Group, error) {
  321. containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  322. Filters: filters.NewArgs(
  323. projectFilter(project.Name),
  324. ),
  325. All: true,
  326. })
  327. if err != nil {
  328. return nil, err
  329. }
  330. var names []string
  331. for _, c := range containers {
  332. names = append(names, getContainerName(c))
  333. }
  334. fmt.Printf("Attaching to %s\n", strings.Join(names, ", "))
  335. eg, ctx := errgroup.WithContext(ctx)
  336. for _, c := range containers {
  337. container := c
  338. eg.Go(func() error {
  339. return s.attachContainer(ctx, container, consumer, project)
  340. })
  341. }
  342. return eg, nil
  343. }
  344. func (s *composeService) attachContainer(ctx context.Context, container moby.Container, consumer compose.LogConsumer, project *types.Project) error {
  345. serviceName := container.Labels[serviceLabel]
  346. w := getWriter(serviceName, container.ID, consumer)
  347. service, err := project.GetService(serviceName)
  348. if err != nil {
  349. return err
  350. }
  351. return s.attachContainerStreams(ctx, container, service.Tty, nil, w)
  352. }
  353. func (s *composeService) attachContainerStreams(ctx context.Context, container moby.Container, tty bool, r io.Reader, w io.Writer) error {
  354. stdin, stdout, err := s.getContainerStreams(ctx, container)
  355. if err != nil {
  356. return err
  357. }
  358. go func() {
  359. <-ctx.Done()
  360. stdout.Close() //nolint:errcheck
  361. stdin.Close() //nolint:errcheck
  362. }()
  363. if r != nil && stdin != nil {
  364. go func() {
  365. io.Copy(stdin, r) //nolint:errcheck
  366. }()
  367. }
  368. if w != nil {
  369. if tty {
  370. _, err = io.Copy(w, stdout)
  371. } else {
  372. _, err = stdcopy.StdCopy(w, w, stdout)
  373. }
  374. }
  375. return err
  376. }
  377. func (s *composeService) getContainerStreams(ctx context.Context, container moby.Container) (io.WriteCloser, io.ReadCloser, error) {
  378. var stdout io.ReadCloser
  379. var stdin io.WriteCloser
  380. if container.State == containerRunning {
  381. logs, err := s.apiClient.ContainerLogs(ctx, container.ID, moby.ContainerLogsOptions{
  382. ShowStdout: true,
  383. ShowStderr: true,
  384. Follow: true,
  385. })
  386. if err != nil {
  387. return nil, nil, err
  388. }
  389. stdout = logs
  390. } else {
  391. cnx, err := s.apiClient.ContainerAttach(ctx, container.ID, moby.ContainerAttachOptions{
  392. Stream: true,
  393. Stdin: true,
  394. Stdout: true,
  395. Stderr: true,
  396. })
  397. if err != nil {
  398. return nil, nil, err
  399. }
  400. stdout = containerStdout{cnx}
  401. stdin = containerStdin{cnx}
  402. }
  403. return stdin, stdout, nil
  404. }
  405. func getContainerName(c moby.Container) string {
  406. // Names return container canonical name /foo + link aliases /linked_by/foo
  407. for _, name := range c.Names {
  408. if strings.LastIndex(name, "/") == 0 {
  409. return name[1:]
  410. }
  411. }
  412. return c.Names[0][1:]
  413. }
  414. func (s *composeService) Down(ctx context.Context, projectName string) error {
  415. eg, _ := errgroup.WithContext(ctx)
  416. w := progress.ContextWriter(ctx)
  417. project, err := s.projectFromContainerLabels(ctx, projectName)
  418. if err != nil {
  419. return err
  420. }
  421. err = InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
  422. filter := filters.NewArgs(projectFilter(project.Name), serviceFilter(service.Name))
  423. return s.removeContainers(ctx, w, eg, filter)
  424. })
  425. if err != nil {
  426. return err
  427. }
  428. err = eg.Wait()
  429. if err != nil {
  430. return err
  431. }
  432. networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{
  433. Filters: filters.NewArgs(
  434. projectFilter(projectName),
  435. ),
  436. })
  437. if err != nil {
  438. return err
  439. }
  440. for _, n := range networks {
  441. networkID := n.ID
  442. networkName := n.Name
  443. eg.Go(func() error {
  444. return s.ensureNetworkDown(ctx, networkID, networkName)
  445. })
  446. }
  447. return eg.Wait()
  448. }
  449. func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, filter filters.Args) error {
  450. containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  451. Filters: filter,
  452. All: true,
  453. })
  454. if err != nil {
  455. return err
  456. }
  457. for _, container := range containers {
  458. eg.Go(func() error {
  459. eventName := "Container " + getContainerName(container)
  460. w.Event(progress.StoppingEvent(eventName))
  461. err := s.apiClient.ContainerStop(ctx, container.ID, nil)
  462. if err != nil {
  463. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  464. return err
  465. }
  466. w.Event(progress.RemovingEvent(eventName))
  467. err = s.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{})
  468. if err != nil {
  469. w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
  470. return err
  471. }
  472. w.Event(progress.RemovedEvent(eventName))
  473. return nil
  474. })
  475. }
  476. return nil
  477. }
  478. func (s *composeService) projectFromContainerLabels(ctx context.Context, projectName string) (*types.Project, error) {
  479. containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  480. Filters: filters.NewArgs(
  481. projectFilter(projectName),
  482. ),
  483. All: true,
  484. })
  485. if err != nil {
  486. return nil, err
  487. }
  488. fakeProject := &types.Project{
  489. Name: projectName,
  490. }
  491. if len(containers) == 0 {
  492. return fakeProject, nil
  493. }
  494. options, err := loadProjectOptionsFromLabels(containers[0])
  495. if err != nil {
  496. return nil, err
  497. }
  498. if options.ConfigPaths[0] == "-" {
  499. for _, container := range containers {
  500. fakeProject.Services = append(fakeProject.Services, types.ServiceConfig{
  501. Name: container.Labels[serviceLabel],
  502. })
  503. }
  504. return fakeProject, nil
  505. }
  506. project, err := cli.ProjectFromOptions(options)
  507. if err != nil {
  508. return nil, err
  509. }
  510. return project, nil
  511. }
  512. func loadProjectOptionsFromLabels(c moby.Container) (*cli.ProjectOptions, error) {
  513. var configFiles []string
  514. relativePathConfigFiles := strings.Split(c.Labels[configFilesLabel], ",")
  515. for _, c := range relativePathConfigFiles {
  516. configFiles = append(configFiles, filepath.Base(c))
  517. }
  518. return cli.NewProjectOptions(configFiles,
  519. cli.WithOsEnv,
  520. cli.WithWorkingDirectory(c.Labels[workingDirLabel]),
  521. cli.WithName(c.Labels[projectLabel]))
  522. }
  523. func (s *composeService) Logs(ctx context.Context, projectName string, consumer compose.LogConsumer) error {
  524. list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  525. Filters: filters.NewArgs(
  526. projectFilter(projectName),
  527. ),
  528. })
  529. if err != nil {
  530. return err
  531. }
  532. eg, ctx := errgroup.WithContext(ctx)
  533. for _, c := range list {
  534. service := c.Labels[serviceLabel]
  535. container, err := s.apiClient.ContainerInspect(ctx, c.ID)
  536. if err != nil {
  537. return err
  538. }
  539. eg.Go(func() error {
  540. r, err := s.apiClient.ContainerLogs(ctx, container.ID, moby.ContainerLogsOptions{
  541. ShowStdout: true,
  542. ShowStderr: true,
  543. Follow: true,
  544. })
  545. defer r.Close() // nolint errcheck
  546. if err != nil {
  547. return err
  548. }
  549. w := getWriter(service, container.ID, consumer)
  550. if container.Config.Tty {
  551. _, err = io.Copy(w, r)
  552. } else {
  553. _, err = stdcopy.StdCopy(w, w, r)
  554. }
  555. return err
  556. })
  557. }
  558. return eg.Wait()
  559. }
  560. type splitBuffer struct {
  561. service string
  562. container string
  563. consumer compose.LogConsumer
  564. }
  565. // getWriter creates a io.Writer that will actually split by line and format by LogConsumer
  566. func getWriter(service, container string, l compose.LogConsumer) io.Writer {
  567. return splitBuffer{
  568. service: service,
  569. container: container,
  570. consumer: l,
  571. }
  572. }
  573. func (s splitBuffer) Write(b []byte) (n int, err error) {
  574. split := bytes.Split(b, []byte{'\n'})
  575. for _, line := range split {
  576. if len(line) != 0 {
  577. s.consumer.Log(s.service, s.container, string(line))
  578. }
  579. }
  580. return len(b), nil
  581. }
  582. func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) {
  583. list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  584. Filters: filters.NewArgs(
  585. projectFilter(projectName),
  586. ),
  587. })
  588. if err != nil {
  589. return nil, err
  590. }
  591. return containersToServiceStatus(list)
  592. }
  593. func containersToServiceStatus(containers []moby.Container) ([]compose.ServiceStatus, error) {
  594. containersByLabel, keys, err := groupContainerByLabel(containers, serviceLabel)
  595. if err != nil {
  596. return nil, err
  597. }
  598. var services []compose.ServiceStatus
  599. for _, service := range keys {
  600. containers := containersByLabel[service]
  601. runnningContainers := []moby.Container{}
  602. for _, container := range containers {
  603. if container.State == containerRunning {
  604. runnningContainers = append(runnningContainers, container)
  605. }
  606. }
  607. services = append(services, compose.ServiceStatus{
  608. ID: service,
  609. Name: service,
  610. Desired: len(containers),
  611. Replicas: len(runnningContainers),
  612. })
  613. }
  614. return services, nil
  615. }
  616. func groupContainerByLabel(containers []moby.Container, labelName string) (map[string][]moby.Container, []string, error) {
  617. containersByLabel := map[string][]moby.Container{}
  618. keys := []string{}
  619. for _, c := range containers {
  620. label, ok := c.Labels[labelName]
  621. if !ok {
  622. return nil, nil, fmt.Errorf("No label %q set on container %q of compose project", labelName, c.ID)
  623. }
  624. labelContainers, ok := containersByLabel[label]
  625. if !ok {
  626. labelContainers = []moby.Container{}
  627. keys = append(keys, label)
  628. }
  629. labelContainers = append(labelContainers, c)
  630. containersByLabel[label] = labelContainers
  631. }
  632. sort.Strings(keys)
  633. return containersByLabel, keys, nil
  634. }
  635. func (s *composeService) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
  636. list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  637. Filters: filters.NewArgs(hasProjectLabelFilter()),
  638. })
  639. if err != nil {
  640. return nil, err
  641. }
  642. return containersToStacks(list)
  643. }
  644. func containersToStacks(containers []moby.Container) ([]compose.Stack, error) {
  645. containersByLabel, keys, err := groupContainerByLabel(containers, projectLabel)
  646. if err != nil {
  647. return nil, err
  648. }
  649. var projects []compose.Stack
  650. for _, project := range keys {
  651. projects = append(projects, compose.Stack{
  652. ID: project,
  653. Name: project,
  654. Status: combinedStatus(containerToState(containersByLabel[project])),
  655. })
  656. }
  657. return projects, nil
  658. }
  659. func containerToState(containers []moby.Container) []string {
  660. statuses := []string{}
  661. for _, c := range containers {
  662. statuses = append(statuses, c.State)
  663. }
  664. return statuses
  665. }
  666. func combinedStatus(statuses []string) string {
  667. nbByStatus := map[string]int{}
  668. keys := []string{}
  669. for _, status := range statuses {
  670. nb, ok := nbByStatus[status]
  671. if !ok {
  672. nb = 0
  673. keys = append(keys, status)
  674. }
  675. nbByStatus[status] = nb + 1
  676. }
  677. sort.Strings(keys)
  678. result := ""
  679. for _, status := range keys {
  680. nb := nbByStatus[status]
  681. if result != "" {
  682. result = result + ", "
  683. }
  684. result = result + fmt.Sprintf("%s(%d)", status, nb)
  685. }
  686. return result
  687. }
  688. func (s *composeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
  689. switch format {
  690. case "json":
  691. return json.MarshalIndent(project, "", " ")
  692. case "yaml":
  693. return yaml.Marshal(project)
  694. default:
  695. return nil, fmt.Errorf("unsupported format %q", format)
  696. }
  697. }
  698. func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
  699. hash, err := jsonHash(s)
  700. if err != nil {
  701. return nil, nil, nil, err
  702. }
  703. // TODO: change oneoffLabel value for containers started with `docker compose run`
  704. labels := map[string]string{
  705. projectLabel: p.Name,
  706. serviceLabel: s.Name,
  707. versionLabel: ComposeVersion,
  708. oneoffLabel: "False",
  709. configHashLabel: hash,
  710. workingDirLabel: p.WorkingDir,
  711. configFilesLabel: strings.Join(p.ComposeFiles, ","),
  712. containerNumberLabel: strconv.Itoa(number),
  713. }
  714. var (
  715. runCmd strslice.StrSlice
  716. entrypoint strslice.StrSlice
  717. )
  718. if len(s.Command) > 0 {
  719. runCmd = strslice.StrSlice(s.Command)
  720. }
  721. if len(s.Entrypoint) > 0 {
  722. entrypoint = strslice.StrSlice(s.Entrypoint)
  723. }
  724. image := s.Image
  725. if s.Image == "" {
  726. image = fmt.Sprintf("%s_%s", p.Name, s.Name)
  727. }
  728. var (
  729. tty = s.Tty
  730. stdinOpen = s.StdinOpen
  731. attachStdin = false
  732. )
  733. containerConfig := container.Config{
  734. Hostname: s.Hostname,
  735. Domainname: s.DomainName,
  736. User: s.User,
  737. ExposedPorts: buildContainerPorts(s),
  738. Tty: tty,
  739. OpenStdin: stdinOpen,
  740. StdinOnce: true,
  741. AttachStdin: attachStdin,
  742. AttachStderr: true,
  743. AttachStdout: true,
  744. Cmd: runCmd,
  745. Image: image,
  746. WorkingDir: s.WorkingDir,
  747. Entrypoint: entrypoint,
  748. NetworkDisabled: s.NetworkMode == "disabled",
  749. MacAddress: s.MacAddress,
  750. Labels: labels,
  751. StopSignal: s.StopSignal,
  752. Env: toMobyEnv(s.Environment),
  753. Healthcheck: toMobyHealthCheck(s.HealthCheck),
  754. // Volumes: // FIXME unclear to me the overlap with HostConfig.Mounts
  755. StopTimeout: toSeconds(s.StopGracePeriod),
  756. }
  757. mountOptions, err := buildContainerMountOptions(s, inherit)
  758. if err != nil {
  759. return nil, nil, nil, err
  760. }
  761. bindings := buildContainerBindingOptions(s)
  762. networkMode := getNetworkMode(p, s)
  763. hostConfig := container.HostConfig{
  764. Mounts: mountOptions,
  765. CapAdd: strslice.StrSlice(s.CapAdd),
  766. CapDrop: strslice.StrSlice(s.CapDrop),
  767. NetworkMode: networkMode,
  768. Init: s.Init,
  769. ReadonlyRootfs: s.ReadOnly,
  770. // ShmSize: , TODO
  771. Sysctls: s.Sysctls,
  772. PortBindings: bindings,
  773. }
  774. networkConfig := buildDefaultNetworkConfig(s, networkMode)
  775. return &containerConfig, &hostConfig, networkConfig, nil
  776. }
  777. func buildContainerPorts(s types.ServiceConfig) nat.PortSet {
  778. ports := nat.PortSet{}
  779. for _, p := range s.Ports {
  780. p := nat.Port(fmt.Sprintf("%d/%s", p.Target, p.Protocol))
  781. ports[p] = struct{}{}
  782. }
  783. return ports
  784. }
  785. func buildContainerBindingOptions(s types.ServiceConfig) nat.PortMap {
  786. bindings := nat.PortMap{}
  787. for _, port := range s.Ports {
  788. p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
  789. bind := []nat.PortBinding{}
  790. binding := nat.PortBinding{}
  791. if port.Published > 0 {
  792. binding.HostPort = fmt.Sprint(port.Published)
  793. }
  794. bind = append(bind, binding)
  795. bindings[p] = bind
  796. }
  797. return bindings
  798. }
  799. func buildContainerMountOptions(s types.ServiceConfig, inherit *moby.Container) ([]mount.Mount, error) {
  800. mounts := []mount.Mount{}
  801. var inherited []string
  802. if inherit != nil {
  803. for _, m := range inherit.Mounts {
  804. if m.Type == "tmpfs" {
  805. continue
  806. }
  807. src := m.Source
  808. if m.Type == "volume" {
  809. src = m.Name
  810. }
  811. mounts = append(mounts, mount.Mount{
  812. Type: m.Type,
  813. Source: src,
  814. Target: m.Destination,
  815. ReadOnly: !m.RW,
  816. })
  817. inherited = append(inherited, m.Destination)
  818. }
  819. }
  820. for _, v := range s.Volumes {
  821. if contains(inherited, v.Target) {
  822. continue
  823. }
  824. mount, err := buildMount(v)
  825. if err != nil {
  826. return nil, err
  827. }
  828. mounts = append(mounts, mount)
  829. }
  830. return mounts, nil
  831. }
  832. func buildMount(volume types.ServiceVolumeConfig) (mount.Mount, error) {
  833. source := volume.Source
  834. if volume.Type == "bind" && !filepath.IsAbs(source) {
  835. // volume source has already been prefixed with workdir if required, by compose-go project loader
  836. var err error
  837. source, err = filepath.Abs(source)
  838. if err != nil {
  839. return mount.Mount{}, err
  840. }
  841. }
  842. return mount.Mount{
  843. Type: mount.Type(volume.Type),
  844. Source: source,
  845. Target: volume.Target,
  846. ReadOnly: volume.ReadOnly,
  847. Consistency: mount.Consistency(volume.Consistency),
  848. BindOptions: buildBindOption(volume.Bind),
  849. VolumeOptions: buildVolumeOptions(volume.Volume),
  850. TmpfsOptions: buildTmpfsOptions(volume.Tmpfs),
  851. }, nil
  852. }
  853. func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
  854. if bind == nil {
  855. return nil
  856. }
  857. return &mount.BindOptions{
  858. Propagation: mount.Propagation(bind.Propagation),
  859. // NonRecursive: false, FIXME missing from model ?
  860. }
  861. }
  862. func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {
  863. if vol == nil {
  864. return nil
  865. }
  866. return &mount.VolumeOptions{
  867. NoCopy: vol.NoCopy,
  868. // Labels: , // FIXME missing from model ?
  869. // DriverConfig: , // FIXME missing from model ?
  870. }
  871. }
  872. func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
  873. if tmpfs == nil {
  874. return nil
  875. }
  876. return &mount.TmpfsOptions{
  877. SizeBytes: tmpfs.Size,
  878. // Mode: , // FIXME missing from model ?
  879. }
  880. }
  881. func buildDefaultNetworkConfig(s types.ServiceConfig, networkMode container.NetworkMode) *network.NetworkingConfig {
  882. config := map[string]*network.EndpointSettings{}
  883. net := string(networkMode)
  884. config[net] = &network.EndpointSettings{
  885. Aliases: getAliases(s, s.Networks[net]),
  886. }
  887. return &network.NetworkingConfig{
  888. EndpointsConfig: config,
  889. }
  890. }
  891. func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
  892. aliases := []string{s.Name}
  893. if c != nil {
  894. aliases = append(aliases, c.Aliases...)
  895. }
  896. return aliases
  897. }
  898. func getNetworkMode(p *types.Project, service types.ServiceConfig) container.NetworkMode {
  899. mode := service.NetworkMode
  900. if mode == "" {
  901. if len(p.Networks) > 0 {
  902. for name := range getNetworksForService(service) {
  903. return container.NetworkMode(p.Networks[name].Name)
  904. }
  905. }
  906. return container.NetworkMode("none")
  907. }
  908. // FIXME incomplete implementation
  909. if strings.HasPrefix(mode, "service:") {
  910. panic("Not yet implemented")
  911. }
  912. if strings.HasPrefix(mode, "container:") {
  913. panic("Not yet implemented")
  914. }
  915. return container.NetworkMode(mode)
  916. }
  917. func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
  918. if len(s.Networks) > 0 {
  919. return s.Networks
  920. }
  921. return map[string]*types.ServiceNetworkConfig{"default": nil}
  922. }
  923. func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
  924. _, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
  925. if err != nil {
  926. if errdefs.IsNotFound(err) {
  927. if n.External.External {
  928. return fmt.Errorf("network %s declared as external, but could not be found", n.Name)
  929. }
  930. createOpts := moby.NetworkCreate{
  931. // TODO NameSpace Labels
  932. Labels: n.Labels,
  933. Driver: n.Driver,
  934. Options: n.DriverOpts,
  935. Internal: n.Internal,
  936. Attachable: n.Attachable,
  937. }
  938. if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
  939. createOpts.IPAM = &network.IPAM{}
  940. }
  941. if n.Ipam.Driver != "" {
  942. createOpts.IPAM.Driver = n.Ipam.Driver
  943. }
  944. for _, ipamConfig := range n.Ipam.Config {
  945. config := network.IPAMConfig{
  946. Subnet: ipamConfig.Subnet,
  947. }
  948. createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
  949. }
  950. networkEventName := fmt.Sprintf("Network %q", n.Name)
  951. w := progress.ContextWriter(ctx)
  952. w.Event(progress.CreatingEvent(networkEventName))
  953. if _, err := s.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil {
  954. w.Event(progress.ErrorEvent(networkEventName))
  955. return errors.Wrapf(err, "failed to create network %s", n.Name)
  956. }
  957. w.Event(progress.CreatedEvent(networkEventName))
  958. return nil
  959. }
  960. return err
  961. }
  962. return nil
  963. }
  964. func (s *composeService) ensureNetworkDown(ctx context.Context, networkID string, networkName string) error {
  965. w := progress.ContextWriter(ctx)
  966. eventName := fmt.Sprintf("Network %q", networkName)
  967. w.Event(progress.RemovingEvent(eventName))
  968. if err := s.apiClient.NetworkRemove(ctx, networkID); err != nil {
  969. w.Event(progress.ErrorEvent(eventName))
  970. return errors.Wrapf(err, fmt.Sprintf("failed to create network %s", networkID))
  971. }
  972. w.Event(progress.RemovedEvent(eventName))
  973. return nil
  974. }
  975. func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig) error {
  976. // TODO could identify volume by label vs name
  977. _, err := s.apiClient.VolumeInspect(ctx, volume.Name)
  978. if err != nil {
  979. if !errdefs.IsNotFound(err) {
  980. return err
  981. }
  982. eventName := fmt.Sprintf("Volume %q", volume.Name)
  983. w := progress.ContextWriter(ctx)
  984. w.Event(progress.CreatingEvent(eventName))
  985. // TODO we miss support for driver_opts and labels
  986. _, err := s.apiClient.VolumeCreate(ctx, mobyvolume.VolumeCreateBody{
  987. Labels: volume.Labels,
  988. Name: volume.Name,
  989. Driver: volume.Driver,
  990. DriverOpts: volume.DriverOpts,
  991. })
  992. if err != nil {
  993. w.Event(progress.ErrorEvent(eventName))
  994. return err
  995. }
  996. w.Event(progress.CreatedEvent(eventName))
  997. }
  998. return nil
  999. }