compose.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  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/json"
  18. "fmt"
  19. "io"
  20. "path/filepath"
  21. "sort"
  22. "strconv"
  23. "strings"
  24. "sync"
  25. "github.com/compose-spec/compose-go/types"
  26. moby "github.com/docker/docker/api/types"
  27. "github.com/docker/docker/api/types/container"
  28. "github.com/docker/docker/api/types/filters"
  29. "github.com/docker/docker/api/types/mount"
  30. "github.com/docker/docker/api/types/network"
  31. "github.com/docker/docker/api/types/strslice"
  32. "github.com/docker/docker/errdefs"
  33. "github.com/docker/docker/pkg/jsonmessage"
  34. "github.com/docker/go-connections/nat"
  35. "github.com/pkg/errors"
  36. "github.com/sanathkr/go-yaml"
  37. "golang.org/x/sync/errgroup"
  38. "github.com/docker/compose-cli/api/compose"
  39. "github.com/docker/compose-cli/api/containers"
  40. "github.com/docker/compose-cli/formatter"
  41. "github.com/docker/compose-cli/progress"
  42. )
  43. func (s *local) Up(ctx context.Context, project *types.Project, detach bool) error {
  44. for k, network := range project.Networks {
  45. if !network.External.External && network.Name != "" {
  46. network.Name = fmt.Sprintf("%s_%s", project.Name, k)
  47. project.Networks[k] = network
  48. }
  49. err := s.ensureNetwork(ctx, network)
  50. if err != nil {
  51. return err
  52. }
  53. }
  54. for k, volume := range project.Volumes {
  55. if !volume.External.External && volume.Name != "" {
  56. volume.Name = fmt.Sprintf("%s_%s", project.Name, k)
  57. project.Volumes[k] = volume
  58. }
  59. err := s.ensureVolume(ctx, volume)
  60. if err != nil {
  61. return err
  62. }
  63. }
  64. for _, service := range project.Services {
  65. err := s.applyPullPolicy(ctx, service)
  66. if err != nil {
  67. return err
  68. }
  69. }
  70. err := inDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
  71. return s.ensureService(c, project, service)
  72. })
  73. return err
  74. }
  75. func getContainerName(c moby.Container) string {
  76. // Names return container canonical name /foo + link aliases /linked_by/foo
  77. for _, name := range c.Names {
  78. if strings.LastIndex(name, "/") == 0 {
  79. return name[1:]
  80. }
  81. }
  82. return c.Names[0][1:]
  83. }
  84. func (s *local) applyPullPolicy(ctx context.Context, service types.ServiceConfig) error {
  85. w := progress.ContextWriter(ctx)
  86. // TODO build vs pull should be controlled by pull policy
  87. // if service.Build {}
  88. if service.Image != "" {
  89. _, _, err := s.containerService.apiClient.ImageInspectWithRaw(ctx, service.Image)
  90. if err != nil {
  91. if errdefs.IsNotFound(err) {
  92. stream, err := s.containerService.apiClient.ImagePull(ctx, service.Image, moby.ImagePullOptions{})
  93. if err != nil {
  94. return err
  95. }
  96. dec := json.NewDecoder(stream)
  97. for {
  98. var jm jsonmessage.JSONMessage
  99. if err := dec.Decode(&jm); err != nil {
  100. if err == io.EOF {
  101. break
  102. }
  103. return err
  104. }
  105. toProgressEvent(jm, w)
  106. }
  107. }
  108. }
  109. }
  110. return nil
  111. }
  112. func toProgressEvent(jm jsonmessage.JSONMessage, w progress.Writer) {
  113. if jm.Progress != nil {
  114. if jm.Progress.Total != 0 {
  115. percentage := int(float64(jm.Progress.Current)/float64(jm.Progress.Total)*100) / 2
  116. numSpaces := 50 - percentage
  117. status := progress.Working
  118. if jm.Status == "Pull complete" {
  119. status = progress.Done
  120. }
  121. w.Event(progress.Event{
  122. ID: jm.ID,
  123. Text: jm.Status,
  124. Status: status,
  125. StatusText: fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces)),
  126. })
  127. } else {
  128. if jm.Error != nil {
  129. w.Event(progress.Event{
  130. ID: jm.ID,
  131. Text: jm.Status,
  132. Status: progress.Error,
  133. StatusText: jm.Error.Message,
  134. })
  135. } else if jm.Status == "Pull complete" || jm.Status == "Already exists" {
  136. w.Event(progress.Event{
  137. ID: jm.ID,
  138. Text: jm.Status,
  139. Status: progress.Done,
  140. })
  141. } else {
  142. w.Event(progress.Event{
  143. ID: jm.ID,
  144. Text: jm.Status,
  145. Status: progress.Working,
  146. })
  147. }
  148. }
  149. }
  150. }
  151. func (s *local) Down(ctx context.Context, projectName string) error {
  152. list, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  153. Filters: filters.NewArgs(
  154. projectFilter(projectName),
  155. ),
  156. })
  157. if err != nil {
  158. return err
  159. }
  160. eg, _ := errgroup.WithContext(ctx)
  161. w := progress.ContextWriter(ctx)
  162. for _, c := range list {
  163. container := c
  164. eg.Go(func() error {
  165. w.Event(progress.Event{
  166. ID: getContainerName(container),
  167. Text: "Stopping",
  168. Status: progress.Working,
  169. })
  170. err := s.containerService.Stop(ctx, container.ID, nil)
  171. if err != nil {
  172. return err
  173. }
  174. w.Event(progress.Event{
  175. ID: getContainerName(container),
  176. Text: "Removing",
  177. Status: progress.Working,
  178. })
  179. err = s.containerService.Delete(ctx, container.ID, containers.DeleteRequest{})
  180. if err != nil {
  181. return err
  182. }
  183. w.Event(progress.Event{
  184. ID: getContainerName(container),
  185. Text: "Removed",
  186. Status: progress.Done,
  187. })
  188. return nil
  189. })
  190. }
  191. return eg.Wait()
  192. }
  193. func (s *local) Logs(ctx context.Context, projectName string, w io.Writer) error {
  194. list, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  195. Filters: filters.NewArgs(
  196. projectFilter(projectName),
  197. ),
  198. })
  199. if err != nil {
  200. return err
  201. }
  202. var wg sync.WaitGroup
  203. consumer := formatter.NewLogConsumer(w)
  204. for _, c := range list {
  205. service := c.Labels[serviceLabel]
  206. containerID := c.ID
  207. go func() {
  208. _ = s.containerService.Logs(ctx, containerID, containers.LogsRequest{
  209. Follow: true,
  210. Writer: consumer.GetWriter(service, containerID),
  211. })
  212. wg.Done()
  213. }()
  214. wg.Add(1)
  215. }
  216. wg.Wait()
  217. return nil
  218. }
  219. func (s *local) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) {
  220. list, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  221. Filters: filters.NewArgs(
  222. projectFilter(projectName),
  223. ),
  224. })
  225. if err != nil {
  226. return nil, err
  227. }
  228. return containersToServiceStatus(list)
  229. }
  230. func containersToServiceStatus(containers []moby.Container) ([]compose.ServiceStatus, error) {
  231. containersByLabel, keys, err := groupContainerByLabel(containers, serviceLabel)
  232. if err != nil {
  233. return nil, err
  234. }
  235. var services []compose.ServiceStatus
  236. for _, service := range keys {
  237. containers := containersByLabel[service]
  238. runnningContainers := []moby.Container{}
  239. for _, container := range containers {
  240. if container.State == "running" {
  241. runnningContainers = append(runnningContainers, container)
  242. }
  243. }
  244. services = append(services, compose.ServiceStatus{
  245. ID: service,
  246. Name: service,
  247. Desired: len(containers),
  248. Replicas: len(runnningContainers),
  249. })
  250. }
  251. return services, nil
  252. }
  253. func groupContainerByLabel(containers []moby.Container, labelName string) (map[string][]moby.Container, []string, error) {
  254. containersByLabel := map[string][]moby.Container{}
  255. keys := []string{}
  256. for _, c := range containers {
  257. label, ok := c.Labels[labelName]
  258. if !ok {
  259. return nil, nil, fmt.Errorf("No label %q set on container %q of compose project", labelName, c.ID)
  260. }
  261. labelContainers, ok := containersByLabel[label]
  262. if !ok {
  263. labelContainers = []moby.Container{}
  264. keys = append(keys, label)
  265. }
  266. labelContainers = append(labelContainers, c)
  267. containersByLabel[label] = labelContainers
  268. }
  269. sort.Strings(keys)
  270. return containersByLabel, keys, nil
  271. }
  272. func (s *local) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
  273. list, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  274. Filters: filters.NewArgs(hasProjectLabelFilter()),
  275. })
  276. if err != nil {
  277. return nil, err
  278. }
  279. return containersToStacks(list)
  280. }
  281. func containersToStacks(containers []moby.Container) ([]compose.Stack, error) {
  282. containersByLabel, keys, err := groupContainerByLabel(containers, projectLabel)
  283. if err != nil {
  284. return nil, err
  285. }
  286. var projects []compose.Stack
  287. for _, project := range keys {
  288. projects = append(projects, compose.Stack{
  289. ID: project,
  290. Name: project,
  291. Status: combinedStatus(containerToState(containersByLabel[project])),
  292. })
  293. }
  294. return projects, nil
  295. }
  296. func containerToState(containers []moby.Container) []string {
  297. statuses := []string{}
  298. for _, c := range containers {
  299. statuses = append(statuses, c.State)
  300. }
  301. return statuses
  302. }
  303. func combinedStatus(statuses []string) string {
  304. nbByStatus := map[string]int{}
  305. keys := []string{}
  306. for _, status := range statuses {
  307. nb, ok := nbByStatus[status]
  308. if !ok {
  309. nb = 0
  310. keys = append(keys, status)
  311. }
  312. nbByStatus[status] = nb + 1
  313. }
  314. sort.Strings(keys)
  315. result := ""
  316. for _, status := range keys {
  317. nb := nbByStatus[status]
  318. if result != "" {
  319. result = result + ", "
  320. }
  321. result = result + fmt.Sprintf("%s(%d)", status, nb)
  322. }
  323. return result
  324. }
  325. func (s *local) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
  326. switch format {
  327. case "json":
  328. return json.MarshalIndent(project, "", " ")
  329. case "yaml":
  330. return yaml.Marshal(project)
  331. default:
  332. return nil, fmt.Errorf("unsupported format %q", format)
  333. }
  334. }
  335. func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
  336. hash, err := jsonHash(s)
  337. if err != nil {
  338. return nil, nil, nil, err
  339. }
  340. labels := map[string]string{
  341. projectLabel: p.Name,
  342. serviceLabel: s.Name,
  343. configHashLabel: hash,
  344. containerNumberLabel: strconv.Itoa(number),
  345. }
  346. var (
  347. runCmd strslice.StrSlice
  348. entrypoint strslice.StrSlice
  349. )
  350. if len(s.Command) > 0 {
  351. runCmd = strslice.StrSlice(s.Command)
  352. }
  353. if len(s.Entrypoint) > 0 {
  354. entrypoint = strslice.StrSlice(s.Entrypoint)
  355. }
  356. image := s.Image
  357. if s.Image == "" {
  358. image = fmt.Sprintf("%s_%s", p.Name, s.Name)
  359. }
  360. var (
  361. tty = s.Tty
  362. stdinOpen = s.StdinOpen
  363. attachStdin = false
  364. )
  365. containerConfig := container.Config{
  366. Hostname: s.Hostname,
  367. Domainname: s.DomainName,
  368. User: s.User,
  369. ExposedPorts: buildContainerPorts(s),
  370. Tty: tty,
  371. OpenStdin: stdinOpen,
  372. StdinOnce: true,
  373. AttachStdin: attachStdin,
  374. AttachStderr: true,
  375. AttachStdout: true,
  376. Cmd: runCmd,
  377. Image: image,
  378. WorkingDir: s.WorkingDir,
  379. Entrypoint: entrypoint,
  380. NetworkDisabled: s.NetworkMode == "disabled",
  381. MacAddress: s.MacAddress,
  382. Labels: labels,
  383. StopSignal: s.StopSignal,
  384. Env: toMobyEnv(s.Environment),
  385. Healthcheck: toMobyHealthCheck(s.HealthCheck),
  386. // Volumes: // FIXME unclear to me the overlap with HostConfig.Mounts
  387. StopTimeout: toSeconds(s.StopGracePeriod),
  388. }
  389. mountOptions := buildContainerMountOptions(p, s, inherit)
  390. bindings := buildContainerBindingOptions(s)
  391. networkMode := getNetworkMode(p, s)
  392. hostConfig := container.HostConfig{
  393. Mounts: mountOptions,
  394. CapAdd: strslice.StrSlice(s.CapAdd),
  395. CapDrop: strslice.StrSlice(s.CapDrop),
  396. NetworkMode: networkMode,
  397. Init: s.Init,
  398. ReadonlyRootfs: s.ReadOnly,
  399. // ShmSize: , TODO
  400. Sysctls: s.Sysctls,
  401. PortBindings: bindings,
  402. }
  403. networkConfig := buildDefaultNetworkConfig(s, networkMode)
  404. return &containerConfig, &hostConfig, networkConfig, nil
  405. }
  406. func buildContainerPorts(s types.ServiceConfig) nat.PortSet {
  407. ports := nat.PortSet{}
  408. for _, p := range s.Ports {
  409. p := nat.Port(fmt.Sprintf("%d/%s", p.Target, p.Protocol))
  410. ports[p] = struct{}{}
  411. }
  412. return ports
  413. }
  414. func buildContainerBindingOptions(s types.ServiceConfig) nat.PortMap {
  415. bindings := nat.PortMap{}
  416. for _, port := range s.Ports {
  417. p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
  418. bind := []nat.PortBinding{}
  419. binding := nat.PortBinding{}
  420. if port.Published > 0 {
  421. binding.HostPort = fmt.Sprint(port.Published)
  422. }
  423. bind = append(bind, binding)
  424. bindings[p] = bind
  425. }
  426. return bindings
  427. }
  428. func buildContainerMountOptions(p *types.Project, s types.ServiceConfig, inherit *moby.Container) []mount.Mount {
  429. mounts := []mount.Mount{}
  430. var inherited []string
  431. if inherit != nil {
  432. for _, m := range inherit.Mounts {
  433. if m.Type == "tmpfs" {
  434. continue
  435. }
  436. src := m.Source
  437. if m.Type == "volume" {
  438. src = m.Name
  439. }
  440. mounts = append(mounts, mount.Mount{
  441. Type: m.Type,
  442. Source: src,
  443. Target: m.Destination,
  444. ReadOnly: !m.RW,
  445. })
  446. inherited = append(inherited, m.Destination)
  447. }
  448. }
  449. for _, v := range s.Volumes {
  450. if contains(inherited, v.Target) {
  451. continue
  452. }
  453. source := v.Source
  454. if v.Type == "bind" && !filepath.IsAbs(source) {
  455. // FIXME handle ~/
  456. source = filepath.Join(p.WorkingDir, source)
  457. }
  458. mounts = append(mounts, mount.Mount{
  459. Type: mount.Type(v.Type),
  460. Source: source,
  461. Target: v.Target,
  462. ReadOnly: v.ReadOnly,
  463. Consistency: mount.Consistency(v.Consistency),
  464. BindOptions: buildBindOption(v.Bind),
  465. VolumeOptions: buildVolumeOptions(v.Volume),
  466. TmpfsOptions: buildTmpfsOptions(v.Tmpfs),
  467. })
  468. }
  469. return mounts
  470. }
  471. func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
  472. if bind == nil {
  473. return nil
  474. }
  475. return &mount.BindOptions{
  476. Propagation: mount.Propagation(bind.Propagation),
  477. // NonRecursive: false, FIXME missing from model ?
  478. }
  479. }
  480. func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {
  481. if vol == nil {
  482. return nil
  483. }
  484. return &mount.VolumeOptions{
  485. NoCopy: vol.NoCopy,
  486. // Labels: , // FIXME missing from model ?
  487. // DriverConfig: , // FIXME missing from model ?
  488. }
  489. }
  490. func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
  491. if tmpfs == nil {
  492. return nil
  493. }
  494. return &mount.TmpfsOptions{
  495. SizeBytes: tmpfs.Size,
  496. // Mode: , // FIXME missing from model ?
  497. }
  498. }
  499. func buildDefaultNetworkConfig(s types.ServiceConfig, networkMode container.NetworkMode) *network.NetworkingConfig {
  500. config := map[string]*network.EndpointSettings{}
  501. net := string(networkMode)
  502. config[net] = &network.EndpointSettings{
  503. Aliases: getAliases(s, s.Networks[net]),
  504. }
  505. return &network.NetworkingConfig{
  506. EndpointsConfig: config,
  507. }
  508. }
  509. func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
  510. aliases := []string{s.Name}
  511. if c != nil {
  512. aliases = append(aliases, c.Aliases...)
  513. }
  514. return aliases
  515. }
  516. func getNetworkMode(p *types.Project, service types.ServiceConfig) container.NetworkMode {
  517. mode := service.NetworkMode
  518. if mode == "" {
  519. if len(p.Networks) > 0 {
  520. for name := range getNetworksForService(service) {
  521. return container.NetworkMode(p.Networks[name].Name)
  522. }
  523. }
  524. return container.NetworkMode("none")
  525. }
  526. /// FIXME incomplete implementation
  527. if strings.HasPrefix(mode, "service:") {
  528. panic("Not yet implemented")
  529. }
  530. if strings.HasPrefix(mode, "container:") {
  531. panic("Not yet implemented")
  532. }
  533. return container.NetworkMode(mode)
  534. }
  535. func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
  536. if len(s.Networks) > 0 {
  537. return s.Networks
  538. }
  539. return map[string]*types.ServiceNetworkConfig{"default": nil}
  540. }
  541. func (s *local) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
  542. _, err := s.containerService.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
  543. if err != nil {
  544. if errdefs.IsNotFound(err) {
  545. createOpts := moby.NetworkCreate{
  546. // TODO NameSpace Labels
  547. Labels: n.Labels,
  548. Driver: n.Driver,
  549. Options: n.DriverOpts,
  550. Internal: n.Internal,
  551. Attachable: n.Attachable,
  552. }
  553. if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
  554. createOpts.IPAM = &network.IPAM{}
  555. }
  556. if n.Ipam.Driver != "" {
  557. createOpts.IPAM.Driver = n.Ipam.Driver
  558. }
  559. for _, ipamConfig := range n.Ipam.Config {
  560. config := network.IPAMConfig{
  561. Subnet: ipamConfig.Subnet,
  562. }
  563. createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
  564. }
  565. w := progress.ContextWriter(ctx)
  566. w.Event(progress.Event{
  567. ID: fmt.Sprintf("Network %q", n.Name),
  568. Status: progress.Working,
  569. StatusText: "Create",
  570. })
  571. if _, err := s.containerService.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil {
  572. return errors.Wrapf(err, "failed to create network %s", n.Name)
  573. }
  574. w.Event(progress.Event{
  575. ID: fmt.Sprintf("Network %q", n.Name),
  576. Status: progress.Done,
  577. StatusText: "Created",
  578. })
  579. return nil
  580. }
  581. return err
  582. }
  583. return nil
  584. }
  585. func (s *local) ensureVolume(ctx context.Context, volume types.VolumeConfig) error {
  586. // TODO could identify volume by label vs name
  587. _, err := s.volumeService.Inspect(ctx, volume.Name)
  588. if err != nil {
  589. if errdefs.IsNotFound(err) {
  590. w := progress.ContextWriter(ctx)
  591. w.Event(progress.Event{
  592. ID: fmt.Sprintf("Volume %q", volume.Name),
  593. Status: progress.Working,
  594. StatusText: "Create",
  595. })
  596. // TODO we miss support for driver_opts and labels
  597. _, err := s.volumeService.Create(ctx, volume.Name, nil)
  598. w.Event(progress.Event{
  599. ID: fmt.Sprintf("Volume %q", volume.Name),
  600. Status: progress.Done,
  601. StatusText: "Created",
  602. })
  603. if err != nil {
  604. return err
  605. }
  606. }
  607. return err
  608. }
  609. return nil
  610. }