create.go 12 KB

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