create.go 18 KB

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