compose.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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. moby "github.com/docker/docker/api/types"
  23. "github.com/docker/docker/api/types/container"
  24. "github.com/docker/docker/api/types/filters"
  25. "github.com/docker/docker/api/types/mount"
  26. "github.com/docker/docker/api/types/network"
  27. "github.com/docker/docker/api/types/strslice"
  28. "github.com/docker/docker/errdefs"
  29. "github.com/docker/go-connections/nat"
  30. "github.com/pkg/errors"
  31. "github.com/sanathkr/go-yaml"
  32. "io"
  33. "path/filepath"
  34. "strings"
  35. "sync"
  36. )
  37. func (s *local) Up(ctx context.Context, project *types.Project, detach bool) error {
  38. for k, network := range project.Networks {
  39. if !network.External.External {
  40. network.Name = fmt.Sprintf("%s_%s", project.Name, k)
  41. project.Networks[k] = network
  42. }
  43. err := s.ensureNetwork(ctx, network)
  44. if err != nil {
  45. return err
  46. }
  47. }
  48. for _, service := range project.Services {
  49. containerConfig, hostConfig, networkingConfig, err := getContainerCreateOptions(project, service)
  50. if err != nil {
  51. return err
  52. }
  53. name := fmt.Sprintf("%s_%s", project.Name, service.Name)
  54. id, err := s.create(ctx, containerConfig, hostConfig, networkingConfig, name)
  55. if err != nil {
  56. return err
  57. }
  58. for net, _ := range service.Networks {
  59. name := fmt.Sprintf("%s_%s", project.Name, net)
  60. err = s.connectContainerToNetwork(ctx, id, service.Name, name)
  61. if err != nil {
  62. return err
  63. }
  64. }
  65. err = s.containerService.apiClient.ContainerStart(ctx, id, moby.ContainerStartOptions{})
  66. if err != nil {
  67. return err
  68. }
  69. }
  70. return nil
  71. }
  72. func (s *local) Down(ctx context.Context, projectName string) error {
  73. list, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  74. Filters: filters.NewArgs(
  75. filters.Arg("label", "com.docker.compose.project="+projectName),
  76. ),
  77. })
  78. if err != nil {
  79. return err
  80. }
  81. for _, c := range list {
  82. s.containerService.Stop(ctx, c.ID, nil)
  83. }
  84. return nil
  85. }
  86. func (s *local) Logs(ctx context.Context, projectName string, w io.Writer) error {
  87. list, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  88. Filters: filters.NewArgs(
  89. filters.Arg("label", "com.docker.compose.project="+projectName),
  90. ),
  91. })
  92. if err != nil {
  93. return err
  94. }
  95. var wg sync.WaitGroup
  96. for _, c := range list {
  97. go func() {
  98. s.containerService.Logs(ctx, c.ID, containers.LogsRequest{
  99. Follow: true,
  100. Writer: w,
  101. })
  102. wg.Done()
  103. }()
  104. wg.Add(1)
  105. }
  106. wg.Wait()
  107. return nil
  108. }
  109. func (s *local) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) {
  110. list, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  111. Filters: filters.NewArgs(
  112. filters.Arg("label", "com.docker.compose.project="+projectName),
  113. ),
  114. })
  115. if err != nil {
  116. return nil, err
  117. }
  118. var status []compose.ServiceStatus
  119. for _,c := range list {
  120. // TODO group by service
  121. status = append(status, compose.ServiceStatus{
  122. ID: c.ID,
  123. Name: c.Labels["com.docker.compose.service"],
  124. Replicas: 0,
  125. Desired: 0,
  126. Ports: nil,
  127. Publishers: nil,
  128. })
  129. }
  130. return status, nil
  131. }
  132. func (s *local) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
  133. _, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{All: true})
  134. if err != nil {
  135. return nil, err
  136. }
  137. var stacks []compose.Stack
  138. // TODO rebuild stacks based on containers
  139. return stacks, nil
  140. }
  141. func (s *local) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
  142. switch format {
  143. case "json":
  144. return json.MarshalIndent(project, "", " ")
  145. case "yaml":
  146. return yaml.Marshal(project)
  147. default:
  148. return nil, fmt.Errorf("unsupported format %q", format)
  149. }
  150. }
  151. func getContainerCreateOptions(p *types.Project, s types.ServiceConfig) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
  152. labels := map[string]string{
  153. "com.docker.compose.project": p.Name,
  154. "com.docker.compose.service": s.Name,
  155. }
  156. var (
  157. runCmd strslice.StrSlice
  158. entrypoint strslice.StrSlice
  159. )
  160. if len(s.Command) > 0 {
  161. runCmd = strslice.StrSlice(s.Command)
  162. }
  163. if len(s.Entrypoint) > 0 {
  164. entrypoint = strslice.StrSlice(s.Entrypoint)
  165. }
  166. image := s.Image
  167. if s.Image == "" {
  168. image = fmt.Sprintf("%s_%s", p.Name, s.Name)
  169. }
  170. var (
  171. tty = s.Tty
  172. stdinOpen = s.StdinOpen
  173. attachStdin = false
  174. )
  175. containerConfig := container.Config{
  176. Hostname: s.Hostname,
  177. Domainname: s.DomainName,
  178. User: s.User,
  179. ExposedPorts: buildContainerPorts(s),
  180. Tty: tty,
  181. OpenStdin: stdinOpen,
  182. StdinOnce: true,
  183. AttachStdin: attachStdin,
  184. AttachStderr: true,
  185. AttachStdout: true,
  186. Cmd: runCmd,
  187. Image: image,
  188. WorkingDir: s.WorkingDir,
  189. Entrypoint: entrypoint,
  190. NetworkDisabled: s.NetworkMode == "disabled",
  191. MacAddress: s.MacAddress,
  192. Labels: labels,
  193. StopSignal: s.StopSignal,
  194. // Env: s.Environment, FIXME conversion
  195. // Healthcheck: s.HealthCheck, FIXME conversion
  196. // Volumes: // FIXME unclear to me the overlap with HostConfig.Mounts
  197. // StopTimeout: s.StopGracePeriod FIXME conversion
  198. }
  199. mountOptions, err := buildContainerMountOptions(p, s)
  200. if err != nil {
  201. return nil, nil, nil, err
  202. }
  203. bindings, err := buildContainerBindingOptions(s)
  204. if err != nil {
  205. return nil, nil, nil, err
  206. }
  207. networkMode := getNetworkMode(p, s)
  208. hostConfig := container.HostConfig{
  209. Mounts: mountOptions,
  210. CapAdd: strslice.StrSlice(s.CapAdd),
  211. CapDrop: strslice.StrSlice(s.CapDrop),
  212. NetworkMode: networkMode,
  213. Init: s.Init,
  214. ReadonlyRootfs: s.ReadOnly,
  215. // ShmSize: , TODO
  216. Sysctls: s.Sysctls,
  217. PortBindings: bindings,
  218. }
  219. networkConfig := buildDefaultNetworkConfig(s, networkMode)
  220. return &containerConfig, &hostConfig, networkConfig, nil
  221. }
  222. func buildContainerPorts(s types.ServiceConfig) nat.PortSet {
  223. ports := nat.PortSet{}
  224. for _, p := range s.Ports {
  225. p := nat.Port(fmt.Sprintf("%d/%s", p.Target, p.Protocol))
  226. ports[p] = struct{}{}
  227. }
  228. return ports
  229. }
  230. func buildContainerBindingOptions(s types.ServiceConfig) (nat.PortMap, error) {
  231. bindings := nat.PortMap{}
  232. for _, port := range s.Ports {
  233. p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
  234. bind := []nat.PortBinding{}
  235. binding := nat.PortBinding{}
  236. if port.Published > 0 {
  237. binding.HostPort = fmt.Sprint(port.Published)
  238. }
  239. bind = append(bind, binding)
  240. bindings[p] = bind
  241. }
  242. return bindings, nil
  243. }
  244. func buildContainerMountOptions(p *types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
  245. mounts := []mount.Mount{}
  246. for _, v := range s.Volumes {
  247. source := v.Source
  248. if v.Type == "bind" && !filepath.IsAbs(source) {
  249. // FIXME handle ~/
  250. source = filepath.Join(p.WorkingDir, source)
  251. }
  252. mounts = append(mounts, mount.Mount{
  253. Type: mount.Type(v.Type),
  254. Source: source,
  255. Target: v.Target,
  256. ReadOnly: v.ReadOnly,
  257. Consistency: mount.Consistency(v.Consistency),
  258. BindOptions: buildBindOption(v.Bind),
  259. VolumeOptions: buildVolumeOptions(v.Volume),
  260. TmpfsOptions: buildTmpfsOptions(v.Tmpfs),
  261. })
  262. }
  263. return mounts, nil
  264. }
  265. func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
  266. if bind == nil {
  267. return nil
  268. }
  269. return &mount.BindOptions{
  270. Propagation: mount.Propagation(bind.Propagation),
  271. // NonRecursive: false, FIXME missing from model ?
  272. }
  273. }
  274. func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {
  275. if vol == nil {
  276. return nil
  277. }
  278. return &mount.VolumeOptions{
  279. NoCopy: vol.NoCopy,
  280. // Labels: , // FIXME missing from model ?
  281. // DriverConfig: , // FIXME missing from model ?
  282. }
  283. }
  284. func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
  285. if tmpfs == nil {
  286. return nil
  287. }
  288. return &mount.TmpfsOptions{
  289. SizeBytes: tmpfs.Size,
  290. // Mode: , // FIXME missing from model ?
  291. }
  292. }
  293. func buildDefaultNetworkConfig(s types.ServiceConfig, networkMode container.NetworkMode) *network.NetworkingConfig {
  294. config := map[string]*network.EndpointSettings{}
  295. net := string(networkMode)
  296. config[net] = &network.EndpointSettings{
  297. Aliases: getAliases(s, s.Networks[net]),
  298. }
  299. return &network.NetworkingConfig{
  300. EndpointsConfig: config,
  301. }
  302. }
  303. func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
  304. aliases := []string{s.Name}
  305. if c != nil {
  306. aliases = append(aliases, c.Aliases...)
  307. }
  308. return aliases
  309. }
  310. func getNetworkMode(p *types.Project, service types.ServiceConfig) container.NetworkMode {
  311. mode := service.NetworkMode
  312. if mode == "" {
  313. if len(p.Networks) > 0 {
  314. for name := range getNetworksForService(service) {
  315. return container.NetworkMode(p.Networks[name].Name)
  316. }
  317. }
  318. return container.NetworkMode("none")
  319. }
  320. /// FIXME incomplete implementation
  321. if strings.HasPrefix(mode, "service:") {
  322. panic("Not yet implemented")
  323. }
  324. if strings.HasPrefix(mode, "container:") {
  325. panic("Not yet implemented")
  326. }
  327. return container.NetworkMode(mode)
  328. }
  329. func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
  330. if len(s.Networks) > 0 {
  331. return s.Networks
  332. }
  333. return map[string]*types.ServiceNetworkConfig{"default": nil}
  334. }
  335. func (s *local) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
  336. _, err := s.containerService.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
  337. if err != nil {
  338. if errdefs.IsNotFound(err) {
  339. createOpts := moby.NetworkCreate{
  340. // TODO NameSpace Labels
  341. Labels: n.Labels,
  342. Driver: n.Driver,
  343. Options: n.DriverOpts,
  344. Internal: n.Internal,
  345. Attachable: n.Attachable,
  346. }
  347. if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
  348. createOpts.IPAM = &network.IPAM{}
  349. }
  350. if n.Ipam.Driver != "" {
  351. createOpts.IPAM.Driver = n.Ipam.Driver
  352. }
  353. for _, ipamConfig := range n.Ipam.Config {
  354. config := network.IPAMConfig{
  355. Subnet: ipamConfig.Subnet,
  356. }
  357. createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
  358. }
  359. if _, err := s.containerService.apiClient.NetworkCreate(context.Background(), n.Name, createOpts); err != nil {
  360. return errors.Wrapf(err, "failed to create network %s", n.Name)
  361. }
  362. return nil
  363. } else {
  364. return err
  365. }
  366. }
  367. return nil
  368. }
  369. func (s *local) connectContainerToNetwork(ctx context.Context, id string, service string, n string) error {
  370. err := s.containerService.apiClient.NetworkConnect(ctx, n, id, &network.EndpointSettings{
  371. Aliases: []string{service},
  372. })
  373. if err != nil {
  374. return err
  375. }
  376. return nil
  377. }