create.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  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. "github.com/compose-spec/compose-go/types"
  21. moby "github.com/docker/docker/api/types"
  22. "github.com/docker/docker/api/types/container"
  23. "github.com/docker/docker/api/types/filters"
  24. "github.com/docker/docker/api/types/mount"
  25. "github.com/docker/docker/api/types/network"
  26. "github.com/docker/docker/api/types/strslice"
  27. volume_api "github.com/docker/docker/api/types/volume"
  28. "github.com/docker/docker/errdefs"
  29. "github.com/docker/go-connections/nat"
  30. "github.com/pkg/errors"
  31. "github.com/sirupsen/logrus"
  32. "golang.org/x/sync/errgroup"
  33. "github.com/docker/compose-cli/api/compose"
  34. "github.com/docker/compose-cli/api/progress"
  35. convert "github.com/docker/compose-cli/local/moby"
  36. )
  37. func (s *composeService) Create(ctx context.Context, project *types.Project, opts compose.CreateOptions) error {
  38. err := s.ensureImagesExists(ctx, project)
  39. if err != nil {
  40. return err
  41. }
  42. prepareNetworks(project)
  43. err = prepareVolumes(project)
  44. if err != nil {
  45. return err
  46. }
  47. if err := s.ensureNetworks(ctx, project.Networks); err != nil {
  48. return err
  49. }
  50. if err := s.ensureProjectVolumes(ctx, project); err != nil {
  51. return err
  52. }
  53. var observedState Containers
  54. observedState, err = s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  55. Filters: filters.NewArgs(
  56. projectFilter(project.Name),
  57. ),
  58. All: true,
  59. })
  60. if err != nil {
  61. return err
  62. }
  63. orphans := observedState.filter(isNotService(project.ServiceNames()...))
  64. if len(orphans) > 0 {
  65. if opts.RemoveOrphans {
  66. eg, _ := errgroup.WithContext(ctx)
  67. w := progress.ContextWriter(ctx)
  68. s.removeContainers(ctx, w, eg, orphans)
  69. if eg.Wait() != nil {
  70. return err
  71. }
  72. } else {
  73. logrus.Warnf("Found orphan containers (%s) for this project. If "+
  74. "you removed or renamed this service in your compose "+
  75. "file, you can run this command with the "+
  76. "--remove-orphans flag to clean it up.", orphans.names())
  77. }
  78. }
  79. return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
  80. return s.ensureService(c, observedState, project, service)
  81. })
  82. }
  83. func prepareVolumes(p *types.Project) error {
  84. for i := range p.Services {
  85. volumesFrom, dependServices, err := getVolumesFrom(p, p.Services[i].VolumesFrom)
  86. if err != nil {
  87. return err
  88. }
  89. p.Services[i].VolumesFrom = volumesFrom
  90. if len(dependServices) > 0 {
  91. if p.Services[i].DependsOn == nil {
  92. p.Services[i].DependsOn = make(types.DependsOnConfig, len(dependServices))
  93. }
  94. for _, service := range p.Services {
  95. if contains(dependServices, service.Name) {
  96. p.Services[i].DependsOn[service.Name] = types.ServiceDependency{
  97. Condition: types.ServiceConditionStarted,
  98. }
  99. }
  100. }
  101. }
  102. }
  103. return nil
  104. }
  105. func prepareNetworks(project *types.Project) {
  106. for k, network := range project.Networks {
  107. network.Labels = network.Labels.Add(networkLabel, k)
  108. network.Labels = network.Labels.Add(projectLabel, project.Name)
  109. network.Labels = network.Labels.Add(versionLabel, ComposeVersion)
  110. project.Networks[k] = network
  111. }
  112. }
  113. func (s *composeService) ensureNetworks(ctx context.Context, networks types.Networks) error {
  114. for _, network := range networks {
  115. err := s.ensureNetwork(ctx, network)
  116. if err != nil {
  117. return err
  118. }
  119. }
  120. return nil
  121. }
  122. func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project) error {
  123. for k, volume := range project.Volumes {
  124. volume.Labels = volume.Labels.Add(volumeLabel, k)
  125. volume.Labels = volume.Labels.Add(projectLabel, project.Name)
  126. volume.Labels = volume.Labels.Add(versionLabel, ComposeVersion)
  127. err := s.ensureVolume(ctx, volume)
  128. if err != nil {
  129. return err
  130. }
  131. }
  132. return nil
  133. }
  134. func getImageName(service types.ServiceConfig, projectName string) string {
  135. imageName := service.Image
  136. if imageName == "" {
  137. imageName = projectName + "_" + service.Name
  138. }
  139. return imageName
  140. }
  141. func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project, service types.ServiceConfig, number int, inherit *moby.Container,
  142. autoRemove bool) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
  143. hash, err := jsonHash(service)
  144. if err != nil {
  145. return nil, nil, nil, err
  146. }
  147. labels := map[string]string{}
  148. for k, v := range service.Labels {
  149. labels[k] = v
  150. }
  151. labels[projectLabel] = p.Name
  152. labels[serviceLabel] = service.Name
  153. labels[versionLabel] = ComposeVersion
  154. if _, ok := service.Labels[oneoffLabel]; !ok {
  155. labels[oneoffLabel] = "False"
  156. }
  157. labels[configHashLabel] = hash
  158. labels[workingDirLabel] = p.WorkingDir
  159. labels[configFilesLabel] = strings.Join(p.ComposeFiles, ",")
  160. labels[containerNumberLabel] = strconv.Itoa(number)
  161. var (
  162. runCmd strslice.StrSlice
  163. entrypoint strslice.StrSlice
  164. )
  165. if len(service.Command) > 0 {
  166. runCmd = strslice.StrSlice(service.Command)
  167. }
  168. if len(service.Entrypoint) > 0 {
  169. entrypoint = strslice.StrSlice(service.Entrypoint)
  170. }
  171. var (
  172. tty = service.Tty
  173. stdinOpen = service.StdinOpen
  174. attachStdin = false
  175. )
  176. volumeMounts, binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit)
  177. if err != nil {
  178. return nil, nil, nil, err
  179. }
  180. containerConfig := container.Config{
  181. Hostname: service.Hostname,
  182. Domainname: service.DomainName,
  183. User: service.User,
  184. ExposedPorts: buildContainerPorts(service),
  185. Tty: tty,
  186. OpenStdin: stdinOpen,
  187. StdinOnce: true,
  188. AttachStdin: attachStdin,
  189. AttachStderr: true,
  190. AttachStdout: true,
  191. Cmd: runCmd,
  192. Image: getImageName(service, p.Name),
  193. WorkingDir: service.WorkingDir,
  194. Entrypoint: entrypoint,
  195. NetworkDisabled: service.NetworkMode == "disabled",
  196. MacAddress: service.MacAddress,
  197. Labels: labels,
  198. StopSignal: service.StopSignal,
  199. Env: convert.ToMobyEnv(service.Environment),
  200. Healthcheck: convert.ToMobyHealthCheck(service.HealthCheck),
  201. Volumes: volumeMounts,
  202. StopTimeout: convert.ToSeconds(service.StopGracePeriod),
  203. }
  204. portBindings := buildContainerPortBindingOptions(service)
  205. resources := getDeployResources(service)
  206. networkMode := getNetworkMode(p, service)
  207. hostConfig := container.HostConfig{
  208. AutoRemove: autoRemove,
  209. Binds: binds,
  210. Mounts: mounts,
  211. CapAdd: strslice.StrSlice(service.CapAdd),
  212. CapDrop: strslice.StrSlice(service.CapDrop),
  213. NetworkMode: networkMode,
  214. Init: service.Init,
  215. ReadonlyRootfs: service.ReadOnly,
  216. // ShmSize: , TODO
  217. Sysctls: service.Sysctls,
  218. PortBindings: portBindings,
  219. Resources: resources,
  220. VolumeDriver: service.VolumeDriver,
  221. VolumesFrom: service.VolumesFrom,
  222. }
  223. networkConfig := buildDefaultNetworkConfig(service, networkMode, getContainerName(p.Name, service, number))
  224. return &containerConfig, &hostConfig, networkConfig, nil
  225. }
  226. func getDeployResources(s types.ServiceConfig) container.Resources {
  227. resources := container.Resources{}
  228. if s.Deploy == nil {
  229. return resources
  230. }
  231. reservations := s.Deploy.Resources.Reservations
  232. if reservations == nil || len(reservations.Devices) == 0 {
  233. return resources
  234. }
  235. for _, device := range reservations.Devices {
  236. resources.DeviceRequests = append(resources.DeviceRequests, container.DeviceRequest{
  237. Capabilities: [][]string{device.Capabilities},
  238. Count: int(device.Count),
  239. DeviceIDs: device.IDs,
  240. Driver: device.Driver,
  241. })
  242. }
  243. return resources
  244. }
  245. func buildContainerPorts(s types.ServiceConfig) nat.PortSet {
  246. ports := nat.PortSet{}
  247. for _, p := range s.Ports {
  248. p := nat.Port(fmt.Sprintf("%d/%s", p.Target, p.Protocol))
  249. ports[p] = struct{}{}
  250. }
  251. return ports
  252. }
  253. func buildContainerPortBindingOptions(s types.ServiceConfig) nat.PortMap {
  254. bindings := nat.PortMap{}
  255. for _, port := range s.Ports {
  256. p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
  257. bind := []nat.PortBinding{}
  258. binding := nat.PortBinding{}
  259. if port.Published > 0 {
  260. binding.HostPort = fmt.Sprint(port.Published)
  261. }
  262. bind = append(bind, binding)
  263. bindings[p] = bind
  264. }
  265. return bindings
  266. }
  267. func getVolumesFrom(project *types.Project, volumesFrom []string) ([]string, []string, error) {
  268. var volumes = []string{}
  269. var services = []string{}
  270. // parse volumes_from
  271. if len(volumesFrom) == 0 {
  272. return volumes, services, nil
  273. }
  274. for _, vol := range volumesFrom {
  275. spec := strings.Split(vol, ":")
  276. if spec[0] == "container" {
  277. volumes = append(volumes, strings.Join(spec[1:], ":"))
  278. continue
  279. }
  280. serviceName := spec[0]
  281. services = append(services, serviceName)
  282. service, err := project.GetService(serviceName)
  283. if err != nil {
  284. return nil, nil, err
  285. }
  286. firstContainer := getContainerName(project.Name, service, 1)
  287. v := fmt.Sprintf("%s:%s", firstContainer, strings.Join(spec[1:], ":"))
  288. volumes = append(volumes, v)
  289. }
  290. return volumes, services, nil
  291. }
  292. func (s *composeService) buildContainerVolumes(ctx context.Context, p types.Project, service types.ServiceConfig,
  293. inherit *moby.Container) (map[string]struct{}, []string, []mount.Mount, error) {
  294. var mounts = []mount.Mount{}
  295. image := getImageName(service, p.Name)
  296. imgInspect, _, err := s.apiClient.ImageInspectWithRaw(ctx, image)
  297. if err != nil {
  298. return nil, nil, nil, err
  299. }
  300. mountOptions, err := buildContainerMountOptions(p, service, imgInspect, inherit)
  301. if err != nil {
  302. return nil, nil, nil, err
  303. }
  304. // filter binds and volumes mount targets
  305. volumeMounts := map[string]struct{}{}
  306. binds := []string{}
  307. for _, m := range mountOptions {
  308. if m.Type == mount.TypeVolume {
  309. volumeMounts[m.Target] = struct{}{}
  310. if m.Source != "" {
  311. binds = append(binds, fmt.Sprintf("%s:%s:rw", m.Source, m.Target))
  312. }
  313. }
  314. }
  315. for _, m := range mountOptions {
  316. if m.Type == mount.TypeBind || m.Type == mount.TypeTmpfs {
  317. mounts = append(mounts, m)
  318. }
  319. }
  320. return volumeMounts, binds, mounts, nil
  321. }
  322. func buildContainerMountOptions(p types.Project, s types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) {
  323. var mounts = map[string]mount.Mount{}
  324. if inherit != nil {
  325. for _, m := range inherit.Mounts {
  326. if m.Type == "tmpfs" {
  327. continue
  328. }
  329. src := m.Source
  330. if m.Type == "volume" {
  331. src = m.Name
  332. }
  333. mounts[m.Destination] = mount.Mount{
  334. Type: m.Type,
  335. Source: src,
  336. Target: m.Destination,
  337. ReadOnly: !m.RW,
  338. }
  339. }
  340. }
  341. if img.ContainerConfig != nil {
  342. for k := range img.ContainerConfig.Volumes {
  343. mount, err := buildMount(p, types.ServiceVolumeConfig{
  344. Type: types.VolumeTypeVolume,
  345. Target: k,
  346. })
  347. if err != nil {
  348. return nil, err
  349. }
  350. mounts[k] = mount
  351. }
  352. }
  353. for _, v := range s.Volumes {
  354. mount, err := buildMount(p, v)
  355. if err != nil {
  356. return nil, err
  357. }
  358. mounts[mount.Target] = mount
  359. }
  360. secrets, err := buildContainerSecretMounts(p, s)
  361. if err != nil {
  362. return nil, err
  363. }
  364. for _, s := range secrets {
  365. if _, found := mounts[s.Target]; found {
  366. continue
  367. }
  368. mounts[s.Target] = s
  369. }
  370. values := make([]mount.Mount, 0, len(mounts))
  371. for _, v := range mounts {
  372. values = append(values, v)
  373. }
  374. return values, nil
  375. }
  376. func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
  377. var mounts = map[string]mount.Mount{}
  378. secretsDir := "/run/secrets"
  379. for _, secret := range s.Secrets {
  380. target := secret.Target
  381. if secret.Target == "" {
  382. target = filepath.Join(secretsDir, secret.Source)
  383. } else if !filepath.IsAbs(secret.Target) {
  384. target = filepath.Join(secretsDir, secret.Target)
  385. }
  386. definedSecret := p.Secrets[secret.Source]
  387. if definedSecret.External.External {
  388. return nil, fmt.Errorf("unsupported external secret %s", definedSecret.Name)
  389. }
  390. mount, err := buildMount(p, types.ServiceVolumeConfig{
  391. Type: types.VolumeTypeBind,
  392. Source: definedSecret.File,
  393. Target: target,
  394. ReadOnly: true,
  395. })
  396. if err != nil {
  397. return nil, err
  398. }
  399. mounts[target] = mount
  400. }
  401. values := make([]mount.Mount, 0, len(mounts))
  402. for _, v := range mounts {
  403. values = append(values, v)
  404. }
  405. return values, nil
  406. }
  407. func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
  408. source := volume.Source
  409. if volume.Type == types.VolumeTypeBind && !filepath.IsAbs(source) {
  410. // volume source has already been prefixed with workdir if required, by compose-go project loader
  411. var err error
  412. source, err = filepath.Abs(source)
  413. if err != nil {
  414. return mount.Mount{}, err
  415. }
  416. }
  417. if volume.Type == types.VolumeTypeVolume {
  418. if volume.Source != "" {
  419. pVolume, ok := project.Volumes[volume.Source]
  420. if ok {
  421. source = pVolume.Name
  422. }
  423. }
  424. }
  425. return mount.Mount{
  426. Type: mount.Type(volume.Type),
  427. Source: source,
  428. Target: volume.Target,
  429. ReadOnly: volume.ReadOnly,
  430. Consistency: mount.Consistency(volume.Consistency),
  431. BindOptions: buildBindOption(volume.Bind),
  432. VolumeOptions: buildVolumeOptions(volume.Volume),
  433. TmpfsOptions: buildTmpfsOptions(volume.Tmpfs),
  434. }, nil
  435. }
  436. func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
  437. if bind == nil {
  438. return nil
  439. }
  440. return &mount.BindOptions{
  441. Propagation: mount.Propagation(bind.Propagation),
  442. // NonRecursive: false, FIXME missing from model ?
  443. }
  444. }
  445. func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {
  446. if vol == nil {
  447. return nil
  448. }
  449. return &mount.VolumeOptions{
  450. NoCopy: vol.NoCopy,
  451. // Labels: , // FIXME missing from model ?
  452. // DriverConfig: , // FIXME missing from model ?
  453. }
  454. }
  455. func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
  456. if tmpfs == nil {
  457. return nil
  458. }
  459. return &mount.TmpfsOptions{
  460. SizeBytes: tmpfs.Size,
  461. // Mode: , // FIXME missing from model ?
  462. }
  463. }
  464. func buildDefaultNetworkConfig(s types.ServiceConfig, networkMode container.NetworkMode, containerName string) *network.NetworkingConfig {
  465. config := map[string]*network.EndpointSettings{}
  466. net := string(networkMode)
  467. config[net] = &network.EndpointSettings{
  468. Aliases: append(getAliases(s, s.Networks[net]), containerName),
  469. }
  470. return &network.NetworkingConfig{
  471. EndpointsConfig: config,
  472. }
  473. }
  474. func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
  475. aliases := []string{s.Name}
  476. if c != nil {
  477. aliases = append(aliases, c.Aliases...)
  478. }
  479. return aliases
  480. }
  481. func getNetworkMode(p *types.Project, service types.ServiceConfig) container.NetworkMode {
  482. mode := service.NetworkMode
  483. if mode == "" {
  484. if len(p.Networks) > 0 {
  485. for name := range getNetworksForService(service) {
  486. return container.NetworkMode(p.Networks[name].Name)
  487. }
  488. }
  489. return container.NetworkMode("none")
  490. }
  491. // FIXME incomplete implementation
  492. if strings.HasPrefix(mode, "service:") {
  493. panic("Not yet implemented")
  494. }
  495. if strings.HasPrefix(mode, "container:") {
  496. panic("Not yet implemented")
  497. }
  498. return container.NetworkMode(mode)
  499. }
  500. func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
  501. if len(s.Networks) > 0 {
  502. return s.Networks
  503. }
  504. return map[string]*types.ServiceNetworkConfig{"default": nil}
  505. }
  506. func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
  507. _, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
  508. if err != nil {
  509. if errdefs.IsNotFound(err) {
  510. if n.External.External {
  511. return fmt.Errorf("network %s declared as external, but could not be found", n.Name)
  512. }
  513. createOpts := moby.NetworkCreate{
  514. // TODO NameSpace Labels
  515. Labels: n.Labels,
  516. Driver: n.Driver,
  517. Options: n.DriverOpts,
  518. Internal: n.Internal,
  519. Attachable: n.Attachable,
  520. }
  521. if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
  522. createOpts.IPAM = &network.IPAM{}
  523. }
  524. if n.Ipam.Driver != "" {
  525. createOpts.IPAM.Driver = n.Ipam.Driver
  526. }
  527. for _, ipamConfig := range n.Ipam.Config {
  528. config := network.IPAMConfig{
  529. Subnet: ipamConfig.Subnet,
  530. }
  531. createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
  532. }
  533. networkEventName := fmt.Sprintf("Network %q", n.Name)
  534. w := progress.ContextWriter(ctx)
  535. w.Event(progress.CreatingEvent(networkEventName))
  536. if _, err := s.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil {
  537. w.Event(progress.ErrorEvent(networkEventName))
  538. return errors.Wrapf(err, "failed to create network %s", n.Name)
  539. }
  540. w.Event(progress.CreatedEvent(networkEventName))
  541. return nil
  542. }
  543. return err
  544. }
  545. return nil
  546. }
  547. func (s *composeService) ensureNetworkDown(ctx context.Context, networkID string, networkName string) error {
  548. w := progress.ContextWriter(ctx)
  549. eventName := fmt.Sprintf("Network %q", networkName)
  550. w.Event(progress.RemovingEvent(eventName))
  551. if err := s.apiClient.NetworkRemove(ctx, networkID); err != nil {
  552. w.Event(progress.ErrorEvent(eventName))
  553. return errors.Wrapf(err, fmt.Sprintf("failed to create network %s", networkID))
  554. }
  555. w.Event(progress.RemovedEvent(eventName))
  556. return nil
  557. }
  558. func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig) error {
  559. // TODO could identify volume by label vs name
  560. _, err := s.apiClient.VolumeInspect(ctx, volume.Name)
  561. if err != nil {
  562. if !errdefs.IsNotFound(err) {
  563. return err
  564. }
  565. eventName := fmt.Sprintf("Volume %q", volume.Name)
  566. w := progress.ContextWriter(ctx)
  567. w.Event(progress.CreatingEvent(eventName))
  568. _, err := s.apiClient.VolumeCreate(ctx, volume_api.VolumeCreateBody{
  569. Labels: volume.Labels,
  570. Name: volume.Name,
  571. Driver: volume.Driver,
  572. DriverOpts: volume.DriverOpts,
  573. })
  574. if err != nil {
  575. w.Event(progress.ErrorEvent(eventName))
  576. return err
  577. }
  578. w.Event(progress.CreatedEvent(eventName))
  579. }
  580. return nil
  581. }