compose.go 15 KB

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