compose.go 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075
  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. "context"
  16. "encoding/base64"
  17. "encoding/json"
  18. "fmt"
  19. "io"
  20. "path/filepath"
  21. "sort"
  22. "strconv"
  23. "strings"
  24. "github.com/compose-spec/compose-go/cli"
  25. "github.com/compose-spec/compose-go/types"
  26. "github.com/docker/buildx/build"
  27. cliconfig "github.com/docker/cli/cli/config"
  28. "github.com/docker/distribution/reference"
  29. moby "github.com/docker/docker/api/types"
  30. "github.com/docker/docker/api/types/container"
  31. "github.com/docker/docker/api/types/filters"
  32. "github.com/docker/docker/api/types/mount"
  33. "github.com/docker/docker/api/types/network"
  34. "github.com/docker/docker/api/types/strslice"
  35. mobyvolume "github.com/docker/docker/api/types/volume"
  36. "github.com/docker/docker/client"
  37. "github.com/docker/docker/errdefs"
  38. "github.com/docker/docker/pkg/jsonmessage"
  39. "github.com/docker/docker/pkg/stdcopy"
  40. "github.com/docker/docker/registry"
  41. "github.com/docker/go-connections/nat"
  42. "github.com/pkg/errors"
  43. "github.com/sanathkr/go-yaml"
  44. "golang.org/x/sync/errgroup"
  45. "github.com/docker/compose-cli/api/compose"
  46. "github.com/docker/compose-cli/config"
  47. errdefs2 "github.com/docker/compose-cli/errdefs"
  48. "github.com/docker/compose-cli/formatter"
  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, w io.Writer) error {
  301. var group *errgroup.Group
  302. if w != nil {
  303. eg, err := s.attach(ctx, project, w)
  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, w io.Writer) (*errgroup.Group, error) {
  321. consumer := formatter.NewLogConsumer(ctx, w)
  322. containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  323. Filters: filters.NewArgs(
  324. projectFilter(project.Name),
  325. ),
  326. All: true,
  327. })
  328. if err != nil {
  329. return nil, err
  330. }
  331. var names []string
  332. for _, c := range containers {
  333. names = append(names, getContainerName(c))
  334. }
  335. fmt.Printf("Attaching to %s\n", strings.Join(names, ", "))
  336. eg, ctx := errgroup.WithContext(ctx)
  337. for _, c := range containers {
  338. container := c
  339. eg.Go(func() error {
  340. return s.attachContainer(ctx, container, consumer, project)
  341. })
  342. }
  343. return eg, nil
  344. }
  345. func (s *composeService) attachContainer(ctx context.Context, container moby.Container, consumer formatter.LogConsumer, project *types.Project) error {
  346. serviceName := container.Labels[serviceLabel]
  347. w := consumer.GetWriter(serviceName, container.ID)
  348. service, err := project.GetService(serviceName)
  349. if err != nil {
  350. return err
  351. }
  352. reader, err := s.getContainerStdout(ctx, container)
  353. if err != nil {
  354. return err
  355. }
  356. go func() {
  357. <-ctx.Done()
  358. reader.Close() //nolint:errcheck
  359. }()
  360. if service.Tty {
  361. _, err = io.Copy(w, reader)
  362. } else {
  363. _, err = stdcopy.StdCopy(w, w, reader)
  364. }
  365. return err
  366. }
  367. func (s *composeService) getContainerStdout(ctx context.Context, container moby.Container) (io.ReadCloser, error) {
  368. var reader io.ReadCloser
  369. if container.State == containerRunning {
  370. logs, err := s.apiClient.ContainerLogs(ctx, container.ID, moby.ContainerLogsOptions{
  371. ShowStdout: true,
  372. ShowStderr: true,
  373. Follow: true,
  374. })
  375. if err != nil {
  376. return nil, err
  377. }
  378. reader = logs
  379. } else {
  380. cnx, err := s.apiClient.ContainerAttach(ctx, container.ID, moby.ContainerAttachOptions{
  381. Stream: true,
  382. Stdin: true,
  383. Stdout: true,
  384. Stderr: true,
  385. })
  386. if err != nil {
  387. return nil, err
  388. }
  389. reader = containerStdout{cnx}
  390. err = s.apiClient.ContainerStart(ctx, container.ID, moby.ContainerStartOptions{})
  391. if err != nil {
  392. return nil, err
  393. }
  394. }
  395. return reader, nil
  396. }
  397. func getContainerName(c moby.Container) string {
  398. // Names return container canonical name /foo + link aliases /linked_by/foo
  399. for _, name := range c.Names {
  400. if strings.LastIndex(name, "/") == 0 {
  401. return name[1:]
  402. }
  403. }
  404. return c.Names[0][1:]
  405. }
  406. func (s *composeService) Down(ctx context.Context, projectName string) error {
  407. eg, _ := errgroup.WithContext(ctx)
  408. w := progress.ContextWriter(ctx)
  409. project, err := s.projectFromContainerLabels(ctx, projectName)
  410. if err != nil || project == nil {
  411. return err
  412. }
  413. err = InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
  414. filter := filters.NewArgs(projectFilter(project.Name), serviceFilter(service.Name))
  415. return s.removeContainers(ctx, w, eg, filter)
  416. })
  417. if err != nil {
  418. return err
  419. }
  420. err = eg.Wait()
  421. if err != nil {
  422. return err
  423. }
  424. networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{
  425. Filters: filters.NewArgs(
  426. projectFilter(projectName),
  427. ),
  428. })
  429. if err != nil {
  430. return err
  431. }
  432. for _, n := range networks {
  433. networkID := n.ID
  434. networkName := n.Name
  435. eg.Go(func() error {
  436. return s.ensureNetworkDown(ctx, networkID, networkName)
  437. })
  438. }
  439. return eg.Wait()
  440. }
  441. func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, filter filters.Args) error {
  442. containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  443. Filters: filter,
  444. All: true,
  445. })
  446. if err != nil {
  447. return err
  448. }
  449. for _, container := range containers {
  450. eg.Go(func() error {
  451. eventName := "Container " + getContainerName(container)
  452. w.Event(progress.StoppingEvent(eventName))
  453. err := s.apiClient.ContainerStop(ctx, container.ID, nil)
  454. if err != nil {
  455. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  456. return err
  457. }
  458. w.Event(progress.RemovingEvent(eventName))
  459. err = s.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{})
  460. if err != nil {
  461. w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
  462. return err
  463. }
  464. w.Event(progress.RemovedEvent(eventName))
  465. return nil
  466. })
  467. }
  468. return nil
  469. }
  470. func (s *composeService) projectFromContainerLabels(ctx context.Context, projectName string) (*types.Project, error) {
  471. containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  472. Filters: filters.NewArgs(
  473. projectFilter(projectName),
  474. ),
  475. All: true,
  476. })
  477. if err != nil {
  478. return nil, err
  479. }
  480. if len(containers) == 0 {
  481. return nil, nil
  482. }
  483. options, err := loadProjectOptionsFromLabels(containers[0])
  484. if err != nil {
  485. return nil, err
  486. }
  487. if options.ConfigPaths[0] == "-" {
  488. fakeProject := &types.Project{
  489. Name: projectName,
  490. }
  491. for _, container := range containers {
  492. fakeProject.Services = append(fakeProject.Services, types.ServiceConfig{
  493. Name: container.Labels[serviceLabel],
  494. })
  495. }
  496. return fakeProject, nil
  497. }
  498. project, err := cli.ProjectFromOptions(options)
  499. if err != nil {
  500. return nil, err
  501. }
  502. return project, nil
  503. }
  504. func loadProjectOptionsFromLabels(c moby.Container) (*cli.ProjectOptions, error) {
  505. var configFiles []string
  506. relativePathConfigFiles := strings.Split(c.Labels[configFilesLabel], ",")
  507. for _, c := range relativePathConfigFiles {
  508. configFiles = append(configFiles, filepath.Base(c))
  509. }
  510. return cli.NewProjectOptions(configFiles,
  511. cli.WithOsEnv,
  512. cli.WithWorkingDirectory(c.Labels[workingDirLabel]),
  513. cli.WithName(c.Labels[projectLabel]))
  514. }
  515. func (s *composeService) Logs(ctx context.Context, projectName string, w io.Writer) error {
  516. list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  517. Filters: filters.NewArgs(
  518. projectFilter(projectName),
  519. ),
  520. })
  521. if err != nil {
  522. return err
  523. }
  524. consumer := formatter.NewLogConsumer(ctx, w)
  525. eg, ctx := errgroup.WithContext(ctx)
  526. for _, c := range list {
  527. service := c.Labels[serviceLabel]
  528. container, err := s.apiClient.ContainerInspect(ctx, c.ID)
  529. if err != nil {
  530. return err
  531. }
  532. eg.Go(func() error {
  533. r, err := s.apiClient.ContainerLogs(ctx, container.ID, moby.ContainerLogsOptions{
  534. ShowStdout: true,
  535. ShowStderr: true,
  536. Follow: true,
  537. })
  538. defer r.Close() // nolint errcheck
  539. if err != nil {
  540. return err
  541. }
  542. w := consumer.GetWriter(service, container.ID)
  543. if container.Config.Tty {
  544. _, err = io.Copy(w, r)
  545. } else {
  546. _, err = stdcopy.StdCopy(w, w, r)
  547. }
  548. return err
  549. })
  550. }
  551. return eg.Wait()
  552. }
  553. func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) {
  554. list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  555. Filters: filters.NewArgs(
  556. projectFilter(projectName),
  557. ),
  558. })
  559. if err != nil {
  560. return nil, err
  561. }
  562. return containersToServiceStatus(list)
  563. }
  564. func containersToServiceStatus(containers []moby.Container) ([]compose.ServiceStatus, error) {
  565. containersByLabel, keys, err := groupContainerByLabel(containers, serviceLabel)
  566. if err != nil {
  567. return nil, err
  568. }
  569. var services []compose.ServiceStatus
  570. for _, service := range keys {
  571. containers := containersByLabel[service]
  572. runnningContainers := []moby.Container{}
  573. for _, container := range containers {
  574. if container.State == containerRunning {
  575. runnningContainers = append(runnningContainers, container)
  576. }
  577. }
  578. services = append(services, compose.ServiceStatus{
  579. ID: service,
  580. Name: service,
  581. Desired: len(containers),
  582. Replicas: len(runnningContainers),
  583. })
  584. }
  585. return services, nil
  586. }
  587. func groupContainerByLabel(containers []moby.Container, labelName string) (map[string][]moby.Container, []string, error) {
  588. containersByLabel := map[string][]moby.Container{}
  589. keys := []string{}
  590. for _, c := range containers {
  591. label, ok := c.Labels[labelName]
  592. if !ok {
  593. return nil, nil, fmt.Errorf("No label %q set on container %q of compose project", labelName, c.ID)
  594. }
  595. labelContainers, ok := containersByLabel[label]
  596. if !ok {
  597. labelContainers = []moby.Container{}
  598. keys = append(keys, label)
  599. }
  600. labelContainers = append(labelContainers, c)
  601. containersByLabel[label] = labelContainers
  602. }
  603. sort.Strings(keys)
  604. return containersByLabel, keys, nil
  605. }
  606. func (s *composeService) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
  607. list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  608. Filters: filters.NewArgs(hasProjectLabelFilter()),
  609. })
  610. if err != nil {
  611. return nil, err
  612. }
  613. return containersToStacks(list)
  614. }
  615. func containersToStacks(containers []moby.Container) ([]compose.Stack, error) {
  616. containersByLabel, keys, err := groupContainerByLabel(containers, projectLabel)
  617. if err != nil {
  618. return nil, err
  619. }
  620. var projects []compose.Stack
  621. for _, project := range keys {
  622. projects = append(projects, compose.Stack{
  623. ID: project,
  624. Name: project,
  625. Status: combinedStatus(containerToState(containersByLabel[project])),
  626. })
  627. }
  628. return projects, nil
  629. }
  630. func containerToState(containers []moby.Container) []string {
  631. statuses := []string{}
  632. for _, c := range containers {
  633. statuses = append(statuses, c.State)
  634. }
  635. return statuses
  636. }
  637. func combinedStatus(statuses []string) string {
  638. nbByStatus := map[string]int{}
  639. keys := []string{}
  640. for _, status := range statuses {
  641. nb, ok := nbByStatus[status]
  642. if !ok {
  643. nb = 0
  644. keys = append(keys, status)
  645. }
  646. nbByStatus[status] = nb + 1
  647. }
  648. sort.Strings(keys)
  649. result := ""
  650. for _, status := range keys {
  651. nb := nbByStatus[status]
  652. if result != "" {
  653. result = result + ", "
  654. }
  655. result = result + fmt.Sprintf("%s(%d)", status, nb)
  656. }
  657. return result
  658. }
  659. func (s *composeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
  660. switch format {
  661. case "json":
  662. return json.MarshalIndent(project, "", " ")
  663. case "yaml":
  664. return yaml.Marshal(project)
  665. default:
  666. return nil, fmt.Errorf("unsupported format %q", format)
  667. }
  668. }
  669. func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
  670. hash, err := jsonHash(s)
  671. if err != nil {
  672. return nil, nil, nil, err
  673. }
  674. // TODO: change oneoffLabel value for containers started with `docker compose run`
  675. labels := map[string]string{
  676. projectLabel: p.Name,
  677. serviceLabel: s.Name,
  678. versionLabel: ComposeVersion,
  679. oneoffLabel: "False",
  680. configHashLabel: hash,
  681. workingDirLabel: p.WorkingDir,
  682. configFilesLabel: strings.Join(p.ComposeFiles, ","),
  683. containerNumberLabel: strconv.Itoa(number),
  684. }
  685. var (
  686. runCmd strslice.StrSlice
  687. entrypoint strslice.StrSlice
  688. )
  689. if len(s.Command) > 0 {
  690. runCmd = strslice.StrSlice(s.Command)
  691. }
  692. if len(s.Entrypoint) > 0 {
  693. entrypoint = strslice.StrSlice(s.Entrypoint)
  694. }
  695. image := s.Image
  696. if s.Image == "" {
  697. image = fmt.Sprintf("%s_%s", p.Name, s.Name)
  698. }
  699. var (
  700. tty = s.Tty
  701. stdinOpen = s.StdinOpen
  702. attachStdin = false
  703. )
  704. containerConfig := container.Config{
  705. Hostname: s.Hostname,
  706. Domainname: s.DomainName,
  707. User: s.User,
  708. ExposedPorts: buildContainerPorts(s),
  709. Tty: tty,
  710. OpenStdin: stdinOpen,
  711. StdinOnce: true,
  712. AttachStdin: attachStdin,
  713. AttachStderr: true,
  714. AttachStdout: true,
  715. Cmd: runCmd,
  716. Image: image,
  717. WorkingDir: s.WorkingDir,
  718. Entrypoint: entrypoint,
  719. NetworkDisabled: s.NetworkMode == "disabled",
  720. MacAddress: s.MacAddress,
  721. Labels: labels,
  722. StopSignal: s.StopSignal,
  723. Env: toMobyEnv(s.Environment),
  724. Healthcheck: toMobyHealthCheck(s.HealthCheck),
  725. // Volumes: // FIXME unclear to me the overlap with HostConfig.Mounts
  726. StopTimeout: toSeconds(s.StopGracePeriod),
  727. }
  728. mountOptions, err := buildContainerMountOptions(s, inherit)
  729. if err != nil {
  730. return nil, nil, nil, err
  731. }
  732. bindings := buildContainerBindingOptions(s)
  733. networkMode := getNetworkMode(p, s)
  734. hostConfig := container.HostConfig{
  735. Mounts: mountOptions,
  736. CapAdd: strslice.StrSlice(s.CapAdd),
  737. CapDrop: strslice.StrSlice(s.CapDrop),
  738. NetworkMode: networkMode,
  739. Init: s.Init,
  740. ReadonlyRootfs: s.ReadOnly,
  741. // ShmSize: , TODO
  742. Sysctls: s.Sysctls,
  743. PortBindings: bindings,
  744. }
  745. networkConfig := buildDefaultNetworkConfig(s, networkMode)
  746. return &containerConfig, &hostConfig, networkConfig, nil
  747. }
  748. func buildContainerPorts(s types.ServiceConfig) nat.PortSet {
  749. ports := nat.PortSet{}
  750. for _, p := range s.Ports {
  751. p := nat.Port(fmt.Sprintf("%d/%s", p.Target, p.Protocol))
  752. ports[p] = struct{}{}
  753. }
  754. return ports
  755. }
  756. func buildContainerBindingOptions(s types.ServiceConfig) nat.PortMap {
  757. bindings := nat.PortMap{}
  758. for _, port := range s.Ports {
  759. p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
  760. bind := []nat.PortBinding{}
  761. binding := nat.PortBinding{}
  762. if port.Published > 0 {
  763. binding.HostPort = fmt.Sprint(port.Published)
  764. }
  765. bind = append(bind, binding)
  766. bindings[p] = bind
  767. }
  768. return bindings
  769. }
  770. func buildContainerMountOptions(s types.ServiceConfig, inherit *moby.Container) ([]mount.Mount, error) {
  771. mounts := []mount.Mount{}
  772. var inherited []string
  773. if inherit != nil {
  774. for _, m := range inherit.Mounts {
  775. if m.Type == "tmpfs" {
  776. continue
  777. }
  778. src := m.Source
  779. if m.Type == "volume" {
  780. src = m.Name
  781. }
  782. mounts = append(mounts, mount.Mount{
  783. Type: m.Type,
  784. Source: src,
  785. Target: m.Destination,
  786. ReadOnly: !m.RW,
  787. })
  788. inherited = append(inherited, m.Destination)
  789. }
  790. }
  791. for _, v := range s.Volumes {
  792. if contains(inherited, v.Target) {
  793. continue
  794. }
  795. mount, err := buildMount(v)
  796. if err != nil {
  797. return nil, err
  798. }
  799. mounts = append(mounts, mount)
  800. }
  801. return mounts, nil
  802. }
  803. func buildMount(volume types.ServiceVolumeConfig) (mount.Mount, error) {
  804. source := volume.Source
  805. if volume.Type == "bind" && !filepath.IsAbs(source) {
  806. // volume source has already been prefixed with workdir if required, by compose-go project loader
  807. var err error
  808. source, err = filepath.Abs(source)
  809. if err != nil {
  810. return mount.Mount{}, err
  811. }
  812. }
  813. return mount.Mount{
  814. Type: mount.Type(volume.Type),
  815. Source: source,
  816. Target: volume.Target,
  817. ReadOnly: volume.ReadOnly,
  818. Consistency: mount.Consistency(volume.Consistency),
  819. BindOptions: buildBindOption(volume.Bind),
  820. VolumeOptions: buildVolumeOptions(volume.Volume),
  821. TmpfsOptions: buildTmpfsOptions(volume.Tmpfs),
  822. }, nil
  823. }
  824. func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
  825. if bind == nil {
  826. return nil
  827. }
  828. return &mount.BindOptions{
  829. Propagation: mount.Propagation(bind.Propagation),
  830. // NonRecursive: false, FIXME missing from model ?
  831. }
  832. }
  833. func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {
  834. if vol == nil {
  835. return nil
  836. }
  837. return &mount.VolumeOptions{
  838. NoCopy: vol.NoCopy,
  839. // Labels: , // FIXME missing from model ?
  840. // DriverConfig: , // FIXME missing from model ?
  841. }
  842. }
  843. func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
  844. if tmpfs == nil {
  845. return nil
  846. }
  847. return &mount.TmpfsOptions{
  848. SizeBytes: tmpfs.Size,
  849. // Mode: , // FIXME missing from model ?
  850. }
  851. }
  852. func buildDefaultNetworkConfig(s types.ServiceConfig, networkMode container.NetworkMode) *network.NetworkingConfig {
  853. config := map[string]*network.EndpointSettings{}
  854. net := string(networkMode)
  855. config[net] = &network.EndpointSettings{
  856. Aliases: getAliases(s, s.Networks[net]),
  857. }
  858. return &network.NetworkingConfig{
  859. EndpointsConfig: config,
  860. }
  861. }
  862. func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
  863. aliases := []string{s.Name}
  864. if c != nil {
  865. aliases = append(aliases, c.Aliases...)
  866. }
  867. return aliases
  868. }
  869. func getNetworkMode(p *types.Project, service types.ServiceConfig) container.NetworkMode {
  870. mode := service.NetworkMode
  871. if mode == "" {
  872. if len(p.Networks) > 0 {
  873. for name := range getNetworksForService(service) {
  874. return container.NetworkMode(p.Networks[name].Name)
  875. }
  876. }
  877. return container.NetworkMode("none")
  878. }
  879. // FIXME incomplete implementation
  880. if strings.HasPrefix(mode, "service:") {
  881. panic("Not yet implemented")
  882. }
  883. if strings.HasPrefix(mode, "container:") {
  884. panic("Not yet implemented")
  885. }
  886. return container.NetworkMode(mode)
  887. }
  888. func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
  889. if len(s.Networks) > 0 {
  890. return s.Networks
  891. }
  892. return map[string]*types.ServiceNetworkConfig{"default": nil}
  893. }
  894. func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
  895. _, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
  896. if err != nil {
  897. if errdefs.IsNotFound(err) {
  898. if n.External.External {
  899. return fmt.Errorf("network %s declared as external, but could not be found", n.Name)
  900. }
  901. createOpts := moby.NetworkCreate{
  902. // TODO NameSpace Labels
  903. Labels: n.Labels,
  904. Driver: n.Driver,
  905. Options: n.DriverOpts,
  906. Internal: n.Internal,
  907. Attachable: n.Attachable,
  908. }
  909. if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
  910. createOpts.IPAM = &network.IPAM{}
  911. }
  912. if n.Ipam.Driver != "" {
  913. createOpts.IPAM.Driver = n.Ipam.Driver
  914. }
  915. for _, ipamConfig := range n.Ipam.Config {
  916. config := network.IPAMConfig{
  917. Subnet: ipamConfig.Subnet,
  918. }
  919. createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
  920. }
  921. networkEventName := fmt.Sprintf("Network %q", n.Name)
  922. w := progress.ContextWriter(ctx)
  923. w.Event(progress.CreatingEvent(networkEventName))
  924. if _, err := s.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil {
  925. w.Event(progress.ErrorEvent(networkEventName))
  926. return errors.Wrapf(err, "failed to create network %s", n.Name)
  927. }
  928. w.Event(progress.CreatedEvent(networkEventName))
  929. return nil
  930. }
  931. return err
  932. }
  933. return nil
  934. }
  935. func (s *composeService) ensureNetworkDown(ctx context.Context, networkID string, networkName string) error {
  936. w := progress.ContextWriter(ctx)
  937. eventName := fmt.Sprintf("Network %q", networkName)
  938. w.Event(progress.RemovingEvent(eventName))
  939. if err := s.apiClient.NetworkRemove(ctx, networkID); err != nil {
  940. w.Event(progress.ErrorEvent(eventName))
  941. return errors.Wrapf(err, fmt.Sprintf("failed to create network %s", networkID))
  942. }
  943. w.Event(progress.RemovedEvent(eventName))
  944. return nil
  945. }
  946. func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig) error {
  947. // TODO could identify volume by label vs name
  948. _, err := s.apiClient.VolumeInspect(ctx, volume.Name)
  949. if err != nil {
  950. if errdefs.IsNotFound(err) {
  951. eventName := fmt.Sprintf("Volume %q", volume.Name)
  952. w := progress.ContextWriter(ctx)
  953. w.Event(progress.CreatingEvent(eventName))
  954. // TODO we miss support for driver_opts and labels
  955. _, err := s.apiClient.VolumeCreate(ctx, mobyvolume.VolumeCreateBody{
  956. Labels: volume.Labels,
  957. Name: volume.Name,
  958. Driver: volume.Driver,
  959. DriverOpts: volume.DriverOpts,
  960. })
  961. if err != nil {
  962. w.Event(progress.ErrorEvent(eventName))
  963. return err
  964. }
  965. w.Event(progress.CreatedEvent(eventName))
  966. }
  967. return err
  968. }
  969. return nil
  970. }