compose.go 15 KB

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