create.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package compose
  14. import (
  15. "context"
  16. "fmt"
  17. "path/filepath"
  18. "strconv"
  19. "strings"
  20. convert "github.com/docker/compose-cli/local/moby"
  21. "github.com/docker/compose-cli/progress"
  22. "github.com/compose-spec/compose-go/types"
  23. moby "github.com/docker/docker/api/types"
  24. "github.com/docker/docker/api/types/container"
  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. volume_api "github.com/docker/docker/api/types/volume"
  29. "github.com/docker/docker/errdefs"
  30. "github.com/docker/go-connections/nat"
  31. "github.com/pkg/errors"
  32. )
  33. func (s *composeService) Create(ctx context.Context, project *types.Project) error {
  34. err := s.ensureImagesExists(ctx, project)
  35. if err != nil {
  36. return err
  37. }
  38. for k, network := range project.Networks {
  39. if !network.External.External && network.Name != "" {
  40. network.Name = fmt.Sprintf("%s_%s", project.Name, k)
  41. project.Networks[k] = network
  42. }
  43. network.Labels = network.Labels.Add(networkLabel, k)
  44. network.Labels = network.Labels.Add(projectLabel, project.Name)
  45. network.Labels = network.Labels.Add(versionLabel, ComposeVersion)
  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. volume.Labels = volume.Labels.Add(volumeLabel, k)
  57. volume.Labels = volume.Labels.Add(projectLabel, project.Name)
  58. volume.Labels = volume.Labels.Add(versionLabel, ComposeVersion)
  59. err := s.ensureVolume(ctx, volume)
  60. if err != nil {
  61. return err
  62. }
  63. }
  64. return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
  65. return s.ensureService(c, project, service)
  66. })
  67. }
  68. func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
  69. hash, err := jsonHash(s)
  70. if err != nil {
  71. return nil, nil, nil, err
  72. }
  73. // TODO: change oneoffLabel value for containers started with `docker compose run`
  74. labels := map[string]string{
  75. projectLabel: p.Name,
  76. serviceLabel: s.Name,
  77. versionLabel: ComposeVersion,
  78. oneoffLabel: "False",
  79. configHashLabel: hash,
  80. workingDirLabel: p.WorkingDir,
  81. configFilesLabel: strings.Join(p.ComposeFiles, ","),
  82. containerNumberLabel: strconv.Itoa(number),
  83. }
  84. var (
  85. runCmd strslice.StrSlice
  86. entrypoint strslice.StrSlice
  87. )
  88. if len(s.Command) > 0 {
  89. runCmd = strslice.StrSlice(s.Command)
  90. }
  91. if len(s.Entrypoint) > 0 {
  92. entrypoint = strslice.StrSlice(s.Entrypoint)
  93. }
  94. image := s.Image
  95. if s.Image == "" {
  96. image = fmt.Sprintf("%s_%s", p.Name, s.Name)
  97. }
  98. var (
  99. tty = s.Tty
  100. stdinOpen = s.StdinOpen
  101. attachStdin = false
  102. )
  103. containerConfig := container.Config{
  104. Hostname: s.Hostname,
  105. Domainname: s.DomainName,
  106. User: s.User,
  107. ExposedPorts: buildContainerPorts(s),
  108. Tty: tty,
  109. OpenStdin: stdinOpen,
  110. StdinOnce: true,
  111. AttachStdin: attachStdin,
  112. AttachStderr: true,
  113. AttachStdout: true,
  114. Cmd: runCmd,
  115. Image: image,
  116. WorkingDir: s.WorkingDir,
  117. Entrypoint: entrypoint,
  118. NetworkDisabled: s.NetworkMode == "disabled",
  119. MacAddress: s.MacAddress,
  120. Labels: labels,
  121. StopSignal: s.StopSignal,
  122. Env: convert.ToMobyEnv(s.Environment),
  123. Healthcheck: convert.ToMobyHealthCheck(s.HealthCheck),
  124. // Volumes: // FIXME unclear to me the overlap with HostConfig.Mounts
  125. StopTimeout: convert.ToSeconds(s.StopGracePeriod),
  126. }
  127. mountOptions, err := buildContainerMountOptions(s, inherit)
  128. if err != nil {
  129. return nil, nil, nil, err
  130. }
  131. bindings := buildContainerBindingOptions(s)
  132. networkMode := getNetworkMode(p, s)
  133. hostConfig := container.HostConfig{
  134. Mounts: mountOptions,
  135. CapAdd: strslice.StrSlice(s.CapAdd),
  136. CapDrop: strslice.StrSlice(s.CapDrop),
  137. NetworkMode: networkMode,
  138. Init: s.Init,
  139. ReadonlyRootfs: s.ReadOnly,
  140. // ShmSize: , TODO
  141. Sysctls: s.Sysctls,
  142. PortBindings: bindings,
  143. }
  144. networkConfig := buildDefaultNetworkConfig(s, networkMode)
  145. return &containerConfig, &hostConfig, networkConfig, nil
  146. }
  147. func buildContainerPorts(s types.ServiceConfig) nat.PortSet {
  148. ports := nat.PortSet{}
  149. for _, p := range s.Ports {
  150. p := nat.Port(fmt.Sprintf("%d/%s", p.Target, p.Protocol))
  151. ports[p] = struct{}{}
  152. }
  153. return ports
  154. }
  155. func buildContainerBindingOptions(s types.ServiceConfig) nat.PortMap {
  156. bindings := nat.PortMap{}
  157. for _, port := range s.Ports {
  158. p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
  159. bind := []nat.PortBinding{}
  160. binding := nat.PortBinding{}
  161. if port.Published > 0 {
  162. binding.HostPort = fmt.Sprint(port.Published)
  163. }
  164. bind = append(bind, binding)
  165. bindings[p] = bind
  166. }
  167. return bindings
  168. }
  169. func buildContainerMountOptions(s types.ServiceConfig, inherit *moby.Container) ([]mount.Mount, error) {
  170. mounts := []mount.Mount{}
  171. var inherited []string
  172. if inherit != nil {
  173. for _, m := range inherit.Mounts {
  174. if m.Type == "tmpfs" {
  175. continue
  176. }
  177. src := m.Source
  178. if m.Type == "volume" {
  179. src = m.Name
  180. }
  181. mounts = append(mounts, mount.Mount{
  182. Type: m.Type,
  183. Source: src,
  184. Target: m.Destination,
  185. ReadOnly: !m.RW,
  186. })
  187. inherited = append(inherited, m.Destination)
  188. }
  189. }
  190. for _, v := range s.Volumes {
  191. if contains(inherited, v.Target) {
  192. continue
  193. }
  194. mount, err := buildMount(v)
  195. if err != nil {
  196. return nil, err
  197. }
  198. mounts = append(mounts, mount)
  199. }
  200. return mounts, nil
  201. }
  202. func buildMount(volume types.ServiceVolumeConfig) (mount.Mount, error) {
  203. source := volume.Source
  204. if volume.Type == "bind" && !filepath.IsAbs(source) {
  205. // volume source has already been prefixed with workdir if required, by compose-go project loader
  206. var err error
  207. source, err = filepath.Abs(source)
  208. if err != nil {
  209. return mount.Mount{}, err
  210. }
  211. }
  212. return mount.Mount{
  213. Type: mount.Type(volume.Type),
  214. Source: source,
  215. Target: volume.Target,
  216. ReadOnly: volume.ReadOnly,
  217. Consistency: mount.Consistency(volume.Consistency),
  218. BindOptions: buildBindOption(volume.Bind),
  219. VolumeOptions: buildVolumeOptions(volume.Volume),
  220. TmpfsOptions: buildTmpfsOptions(volume.Tmpfs),
  221. }, nil
  222. }
  223. func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
  224. if bind == nil {
  225. return nil
  226. }
  227. return &mount.BindOptions{
  228. Propagation: mount.Propagation(bind.Propagation),
  229. // NonRecursive: false, FIXME missing from model ?
  230. }
  231. }
  232. func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {
  233. if vol == nil {
  234. return nil
  235. }
  236. return &mount.VolumeOptions{
  237. NoCopy: vol.NoCopy,
  238. // Labels: , // FIXME missing from model ?
  239. // DriverConfig: , // FIXME missing from model ?
  240. }
  241. }
  242. func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
  243. if tmpfs == nil {
  244. return nil
  245. }
  246. return &mount.TmpfsOptions{
  247. SizeBytes: tmpfs.Size,
  248. // Mode: , // FIXME missing from model ?
  249. }
  250. }
  251. func buildDefaultNetworkConfig(s types.ServiceConfig, networkMode container.NetworkMode) *network.NetworkingConfig {
  252. config := map[string]*network.EndpointSettings{}
  253. net := string(networkMode)
  254. config[net] = &network.EndpointSettings{
  255. Aliases: getAliases(s, s.Networks[net]),
  256. }
  257. return &network.NetworkingConfig{
  258. EndpointsConfig: config,
  259. }
  260. }
  261. func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
  262. aliases := []string{s.Name}
  263. if c != nil {
  264. aliases = append(aliases, c.Aliases...)
  265. }
  266. return aliases
  267. }
  268. func getNetworkMode(p *types.Project, service types.ServiceConfig) container.NetworkMode {
  269. mode := service.NetworkMode
  270. if mode == "" {
  271. if len(p.Networks) > 0 {
  272. for name := range getNetworksForService(service) {
  273. return container.NetworkMode(p.Networks[name].Name)
  274. }
  275. }
  276. return container.NetworkMode("none")
  277. }
  278. // FIXME incomplete implementation
  279. if strings.HasPrefix(mode, "service:") {
  280. panic("Not yet implemented")
  281. }
  282. if strings.HasPrefix(mode, "container:") {
  283. panic("Not yet implemented")
  284. }
  285. return container.NetworkMode(mode)
  286. }
  287. func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
  288. if len(s.Networks) > 0 {
  289. return s.Networks
  290. }
  291. return map[string]*types.ServiceNetworkConfig{"default": nil}
  292. }
  293. func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
  294. _, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
  295. if err != nil {
  296. if errdefs.IsNotFound(err) {
  297. if n.External.External {
  298. return fmt.Errorf("network %s declared as external, but could not be found", n.Name)
  299. }
  300. createOpts := moby.NetworkCreate{
  301. // TODO NameSpace Labels
  302. Labels: n.Labels,
  303. Driver: n.Driver,
  304. Options: n.DriverOpts,
  305. Internal: n.Internal,
  306. Attachable: n.Attachable,
  307. }
  308. if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
  309. createOpts.IPAM = &network.IPAM{}
  310. }
  311. if n.Ipam.Driver != "" {
  312. createOpts.IPAM.Driver = n.Ipam.Driver
  313. }
  314. for _, ipamConfig := range n.Ipam.Config {
  315. config := network.IPAMConfig{
  316. Subnet: ipamConfig.Subnet,
  317. }
  318. createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
  319. }
  320. networkEventName := fmt.Sprintf("Network %q", n.Name)
  321. w := progress.ContextWriter(ctx)
  322. w.Event(progress.CreatingEvent(networkEventName))
  323. if _, err := s.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil {
  324. w.Event(progress.ErrorEvent(networkEventName))
  325. return errors.Wrapf(err, "failed to create network %s", n.Name)
  326. }
  327. w.Event(progress.CreatedEvent(networkEventName))
  328. return nil
  329. }
  330. return err
  331. }
  332. return nil
  333. }
  334. func (s *composeService) ensureNetworkDown(ctx context.Context, networkID string, networkName string) error {
  335. w := progress.ContextWriter(ctx)
  336. eventName := fmt.Sprintf("Network %q", networkName)
  337. w.Event(progress.RemovingEvent(eventName))
  338. if err := s.apiClient.NetworkRemove(ctx, networkID); err != nil {
  339. w.Event(progress.ErrorEvent(eventName))
  340. return errors.Wrapf(err, fmt.Sprintf("failed to create network %s", networkID))
  341. }
  342. w.Event(progress.RemovedEvent(eventName))
  343. return nil
  344. }
  345. func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig) error {
  346. // TODO could identify volume by label vs name
  347. _, err := s.apiClient.VolumeInspect(ctx, volume.Name)
  348. if err != nil {
  349. if !errdefs.IsNotFound(err) {
  350. return err
  351. }
  352. eventName := fmt.Sprintf("Volume %q", volume.Name)
  353. w := progress.ContextWriter(ctx)
  354. w.Event(progress.CreatingEvent(eventName))
  355. // TODO we miss support for driver_opts and labels
  356. _, err := s.apiClient.VolumeCreate(ctx, volume_api.VolumeCreateBody{
  357. Labels: volume.Labels,
  358. Name: volume.Name,
  359. Driver: volume.Driver,
  360. DriverOpts: volume.DriverOpts,
  361. })
  362. if err != nil {
  363. w.Event(progress.ErrorEvent(eventName))
  364. return err
  365. }
  366. w.Event(progress.CreatedEvent(eventName))
  367. }
  368. return nil
  369. }