compose.go 18 KB

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