compose.go 17 KB

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