1
0

compose.go 27 KB

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