compose.go 17 KB

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