compose.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943
  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/base64"
  18. "encoding/json"
  19. "fmt"
  20. "io"
  21. "path/filepath"
  22. "sort"
  23. "strconv"
  24. "strings"
  25. "github.com/compose-spec/compose-go/cli"
  26. "github.com/compose-spec/compose-go/types"
  27. "github.com/docker/buildx/build"
  28. cliconfig "github.com/docker/cli/cli/config"
  29. "github.com/docker/distribution/reference"
  30. moby "github.com/docker/docker/api/types"
  31. "github.com/docker/docker/api/types/container"
  32. "github.com/docker/docker/api/types/filters"
  33. "github.com/docker/docker/api/types/mount"
  34. "github.com/docker/docker/api/types/network"
  35. "github.com/docker/docker/api/types/strslice"
  36. mobyvolume "github.com/docker/docker/api/types/volume"
  37. "github.com/docker/docker/client"
  38. "github.com/docker/docker/errdefs"
  39. "github.com/docker/docker/pkg/jsonmessage"
  40. "github.com/docker/docker/pkg/stdcopy"
  41. "github.com/docker/docker/registry"
  42. "github.com/docker/go-connections/nat"
  43. "github.com/pkg/errors"
  44. "github.com/sanathkr/go-yaml"
  45. "golang.org/x/sync/errgroup"
  46. "github.com/docker/compose-cli/api/compose"
  47. "github.com/docker/compose-cli/config"
  48. "github.com/docker/compose-cli/formatter"
  49. "github.com/docker/compose-cli/progress"
  50. )
  51. type composeService struct {
  52. apiClient *client.Client
  53. }
  54. func (s *composeService) Build(ctx context.Context, project *types.Project) error {
  55. opts := map[string]build.Options{}
  56. for _, service := range project.Services {
  57. if service.Build != nil {
  58. opts[service.Name] = s.toBuildOptions(service, project.WorkingDir)
  59. }
  60. }
  61. return s.build(ctx, project, opts)
  62. }
  63. func (s *composeService) Push(ctx context.Context, project *types.Project) error {
  64. configFile, err := cliconfig.Load(config.Dir(ctx))
  65. if err != nil {
  66. return err
  67. }
  68. eg, ctx := errgroup.WithContext(ctx)
  69. info, err := s.apiClient.Info(ctx)
  70. if err != nil {
  71. return err
  72. }
  73. if info.IndexServerAddress == "" {
  74. info.IndexServerAddress = registry.IndexServer
  75. }
  76. for _, service := range project.Services {
  77. if service.Build == nil {
  78. continue
  79. }
  80. service := service
  81. eg.Go(func() error {
  82. w := progress.ContextWriter(ctx)
  83. ref, err := reference.ParseNormalizedNamed(service.Image)
  84. if err != nil {
  85. return err
  86. }
  87. repoInfo, err := registry.ParseRepositoryInfo(ref)
  88. if err != nil {
  89. return err
  90. }
  91. key := repoInfo.Index.Name
  92. if repoInfo.Index.Official {
  93. key = info.IndexServerAddress
  94. }
  95. authConfig, err := configFile.GetAuthConfig(key)
  96. if err != nil {
  97. return err
  98. }
  99. buf, err := json.Marshal(authConfig)
  100. if err != nil {
  101. return err
  102. }
  103. stream, err := s.apiClient.ImagePush(ctx, service.Image, moby.ImagePushOptions{
  104. RegistryAuth: base64.URLEncoding.EncodeToString(buf),
  105. })
  106. if err != nil {
  107. return err
  108. }
  109. dec := json.NewDecoder(stream)
  110. for {
  111. var jm jsonmessage.JSONMessage
  112. if err := dec.Decode(&jm); err != nil {
  113. if err == io.EOF {
  114. break
  115. }
  116. return err
  117. }
  118. if jm.Error != nil {
  119. return errors.New(jm.Error.Message)
  120. }
  121. toProgressEvent("Pushing "+service.Name, jm, w)
  122. }
  123. return nil
  124. })
  125. }
  126. return eg.Wait()
  127. }
  128. func (s *composeService) Pull(ctx context.Context, project *types.Project) error {
  129. configFile, err := cliconfig.Load(config.Dir(ctx))
  130. if err != nil {
  131. return err
  132. }
  133. info, err := s.apiClient.Info(ctx)
  134. if err != nil {
  135. return err
  136. }
  137. if info.IndexServerAddress == "" {
  138. info.IndexServerAddress = registry.IndexServer
  139. }
  140. w := progress.ContextWriter(ctx)
  141. eg, ctx := errgroup.WithContext(ctx)
  142. for _, srv := range project.Services {
  143. service := srv
  144. eg.Go(func() error {
  145. w.Event(progress.Event{
  146. ID: service.Name,
  147. Status: progress.Working,
  148. Text: "Pulling",
  149. })
  150. ref, err := reference.ParseNormalizedNamed(service.Image)
  151. if err != nil {
  152. return err
  153. }
  154. repoInfo, err := registry.ParseRepositoryInfo(ref)
  155. if err != nil {
  156. return err
  157. }
  158. key := repoInfo.Index.Name
  159. if repoInfo.Index.Official {
  160. key = info.IndexServerAddress
  161. }
  162. authConfig, err := configFile.GetAuthConfig(key)
  163. if err != nil {
  164. return err
  165. }
  166. buf, err := json.Marshal(authConfig)
  167. if err != nil {
  168. return err
  169. }
  170. stream, err := s.apiClient.ImagePull(ctx, service.Image, moby.ImagePullOptions{
  171. RegistryAuth: base64.URLEncoding.EncodeToString(buf),
  172. })
  173. if err != nil {
  174. w.Event(progress.Event{
  175. ID: service.Name,
  176. Status: progress.Error,
  177. Text: "Error",
  178. })
  179. return err
  180. }
  181. dec := json.NewDecoder(stream)
  182. for {
  183. var jm jsonmessage.JSONMessage
  184. if err := dec.Decode(&jm); err != nil {
  185. if err == io.EOF {
  186. break
  187. }
  188. return err
  189. }
  190. if jm.Error != nil {
  191. return errors.New(jm.Error.Message)
  192. }
  193. toPullProgressEvent(service.Name, jm, w)
  194. }
  195. w.Event(progress.Event{
  196. ID: service.Name,
  197. Status: progress.Done,
  198. Text: "Pulled",
  199. })
  200. return nil
  201. })
  202. }
  203. return eg.Wait()
  204. }
  205. func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, w progress.Writer) {
  206. if jm.ID == "" || jm.Progress == nil {
  207. return
  208. }
  209. var (
  210. text string
  211. status = progress.Working
  212. )
  213. text = jm.Progress.String()
  214. if jm.Status == "Pull complete" ||
  215. jm.Status == "Already exists" ||
  216. strings.Contains(jm.Status, "Image is up to date") ||
  217. strings.Contains(jm.Status, "Downloaded newer image") {
  218. status = progress.Done
  219. }
  220. if jm.Error != nil {
  221. status = progress.Error
  222. text = jm.Error.Message
  223. }
  224. w.Event(progress.Event{
  225. ID: jm.ID,
  226. ParentID: parent,
  227. Text: jm.Status,
  228. Status: status,
  229. StatusText: text,
  230. })
  231. }
  232. func toProgressEvent(prefix string, jm jsonmessage.JSONMessage, w progress.Writer) {
  233. if jm.ID == "" {
  234. // skipped
  235. return
  236. }
  237. var (
  238. text string
  239. status = progress.Working
  240. )
  241. if jm.Status == "Pull complete" || jm.Status == "Already exists" {
  242. status = progress.Done
  243. }
  244. if jm.Error != nil {
  245. status = progress.Error
  246. text = jm.Error.Message
  247. }
  248. if jm.Progress != nil {
  249. text = jm.Progress.String()
  250. }
  251. w.Event(progress.Event{
  252. ID: fmt.Sprintf("Pushing %s: %s", prefix, jm.ID),
  253. Text: jm.Status,
  254. Status: status,
  255. StatusText: text,
  256. })
  257. }
  258. func (s *composeService) Up(ctx context.Context, project *types.Project, detach bool) error {
  259. err := s.ensureImagesExists(ctx, project)
  260. if err != nil {
  261. return err
  262. }
  263. for k, network := range project.Networks {
  264. if !network.External.External && network.Name == k {
  265. network.Name = fmt.Sprintf("%s_%s", project.Name, k)
  266. project.Networks[k] = network
  267. }
  268. network.Labels = network.Labels.Add(networkLabel, k)
  269. network.Labels = network.Labels.Add(projectLabel, project.Name)
  270. network.Labels = network.Labels.Add(versionLabel, ComposeVersion)
  271. err := s.ensureNetwork(ctx, network)
  272. if err != nil {
  273. return err
  274. }
  275. }
  276. for k, volume := range project.Volumes {
  277. if !volume.External.External && volume.Name != "" {
  278. volume.Name = fmt.Sprintf("%s_%s", project.Name, k)
  279. project.Volumes[k] = volume
  280. }
  281. volume.Labels = volume.Labels.Add(volumeLabel, k)
  282. volume.Labels = volume.Labels.Add(projectLabel, project.Name)
  283. volume.Labels = volume.Labels.Add(versionLabel, ComposeVersion)
  284. err := s.ensureVolume(ctx, volume)
  285. if err != nil {
  286. return err
  287. }
  288. }
  289. err = InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
  290. return s.ensureService(c, project, service)
  291. })
  292. return err
  293. }
  294. func getContainerName(c moby.Container) string {
  295. // Names return container canonical name /foo + link aliases /linked_by/foo
  296. for _, name := range c.Names {
  297. if strings.LastIndex(name, "/") == 0 {
  298. return name[1:]
  299. }
  300. }
  301. return c.Names[0][1:]
  302. }
  303. func (s *composeService) Down(ctx context.Context, projectName string) error {
  304. eg, _ := errgroup.WithContext(ctx)
  305. w := progress.ContextWriter(ctx)
  306. project, err := s.projectFromContainerLabels(ctx, projectName)
  307. if err != nil || project == nil {
  308. return err
  309. }
  310. err = InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
  311. filter := filters.NewArgs(projectFilter(project.Name), serviceFilter(service.Name))
  312. return s.removeContainers(ctx, w, eg, filter)
  313. })
  314. if err != nil {
  315. return err
  316. }
  317. err = eg.Wait()
  318. if err != nil {
  319. return err
  320. }
  321. networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{
  322. Filters: filters.NewArgs(
  323. projectFilter(projectName),
  324. ),
  325. })
  326. if err != nil {
  327. return err
  328. }
  329. for _, n := range networks {
  330. networkID := n.ID
  331. networkName := n.Name
  332. eg.Go(func() error {
  333. return s.ensureNetworkDown(ctx, networkID, networkName)
  334. })
  335. }
  336. return eg.Wait()
  337. }
  338. func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, filter filters.Args) error {
  339. containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  340. Filters: filter,
  341. })
  342. if err != nil {
  343. return err
  344. }
  345. for _, container := range containers {
  346. eg.Go(func() error {
  347. eventName := "Container " + getContainerName(container)
  348. w.Event(progress.StoppingEvent(eventName))
  349. err := s.apiClient.ContainerStop(ctx, container.ID, nil)
  350. if err != nil {
  351. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  352. return err
  353. }
  354. w.Event(progress.RemovingEvent(eventName))
  355. err = s.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{})
  356. if err != nil {
  357. w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
  358. return err
  359. }
  360. w.Event(progress.RemovedEvent(eventName))
  361. return nil
  362. })
  363. }
  364. return nil
  365. }
  366. func (s *composeService) projectFromContainerLabels(ctx context.Context, projectName string) (*types.Project, error) {
  367. containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  368. Filters: filters.NewArgs(
  369. projectFilter(projectName),
  370. ),
  371. })
  372. if err != nil {
  373. return nil, err
  374. }
  375. if len(containers) == 0 {
  376. return nil, nil
  377. }
  378. options, err := loadProjectOptionsFromLabels(containers[0])
  379. if err != nil {
  380. return nil, err
  381. }
  382. if options.ConfigPaths[0] == "-" {
  383. fakeProject := &types.Project{
  384. Name: projectName,
  385. }
  386. for _, container := range containers {
  387. fakeProject.Services = append(fakeProject.Services, types.ServiceConfig{
  388. Name: container.Labels[serviceLabel],
  389. })
  390. }
  391. return fakeProject, nil
  392. }
  393. project, err := cli.ProjectFromOptions(options)
  394. if err != nil {
  395. return nil, err
  396. }
  397. return project, nil
  398. }
  399. func loadProjectOptionsFromLabels(c moby.Container) (*cli.ProjectOptions, error) {
  400. var configFiles []string
  401. relativePathConfigFiles := strings.Split(c.Labels[configFilesLabel], ",")
  402. for _, c := range relativePathConfigFiles {
  403. configFiles = append(configFiles, filepath.Base(c))
  404. }
  405. return cli.NewProjectOptions(configFiles,
  406. cli.WithOsEnv,
  407. cli.WithWorkingDirectory(c.Labels[workingDirLabel]),
  408. cli.WithName(c.Labels[projectLabel]))
  409. }
  410. func (s *composeService) Logs(ctx context.Context, projectName string, w io.Writer) error {
  411. list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  412. Filters: filters.NewArgs(
  413. projectFilter(projectName),
  414. ),
  415. })
  416. if err != nil {
  417. return err
  418. }
  419. consumer := formatter.NewLogConsumer(w)
  420. eg, ctx := errgroup.WithContext(ctx)
  421. for _, c := range list {
  422. service := c.Labels[serviceLabel]
  423. container, err := s.apiClient.ContainerInspect(ctx, c.ID)
  424. if err != nil {
  425. return err
  426. }
  427. eg.Go(func() error {
  428. r, err := s.apiClient.ContainerLogs(ctx, container.ID, moby.ContainerLogsOptions{
  429. ShowStdout: true,
  430. ShowStderr: true,
  431. Follow: true,
  432. })
  433. defer r.Close() // nolint errcheck
  434. if err != nil {
  435. return err
  436. }
  437. w := consumer.GetWriter(service, container.ID)
  438. if container.Config.Tty {
  439. _, err = io.Copy(w, r)
  440. } else {
  441. _, err = stdcopy.StdCopy(w, w, r)
  442. }
  443. return err
  444. })
  445. }
  446. return eg.Wait()
  447. }
  448. func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) {
  449. list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  450. Filters: filters.NewArgs(
  451. projectFilter(projectName),
  452. ),
  453. })
  454. if err != nil {
  455. return nil, err
  456. }
  457. return containersToServiceStatus(list)
  458. }
  459. func containersToServiceStatus(containers []moby.Container) ([]compose.ServiceStatus, error) {
  460. containersByLabel, keys, err := groupContainerByLabel(containers, serviceLabel)
  461. if err != nil {
  462. return nil, err
  463. }
  464. var services []compose.ServiceStatus
  465. for _, service := range keys {
  466. containers := containersByLabel[service]
  467. runnningContainers := []moby.Container{}
  468. for _, container := range containers {
  469. if container.State == "running" {
  470. runnningContainers = append(runnningContainers, container)
  471. }
  472. }
  473. services = append(services, compose.ServiceStatus{
  474. ID: service,
  475. Name: service,
  476. Desired: len(containers),
  477. Replicas: len(runnningContainers),
  478. })
  479. }
  480. return services, nil
  481. }
  482. func groupContainerByLabel(containers []moby.Container, labelName string) (map[string][]moby.Container, []string, error) {
  483. containersByLabel := map[string][]moby.Container{}
  484. keys := []string{}
  485. for _, c := range containers {
  486. label, ok := c.Labels[labelName]
  487. if !ok {
  488. return nil, nil, fmt.Errorf("No label %q set on container %q of compose project", labelName, c.ID)
  489. }
  490. labelContainers, ok := containersByLabel[label]
  491. if !ok {
  492. labelContainers = []moby.Container{}
  493. keys = append(keys, label)
  494. }
  495. labelContainers = append(labelContainers, c)
  496. containersByLabel[label] = labelContainers
  497. }
  498. sort.Strings(keys)
  499. return containersByLabel, keys, nil
  500. }
  501. func (s *composeService) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
  502. list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  503. Filters: filters.NewArgs(hasProjectLabelFilter()),
  504. })
  505. if err != nil {
  506. return nil, err
  507. }
  508. return containersToStacks(list)
  509. }
  510. func containersToStacks(containers []moby.Container) ([]compose.Stack, error) {
  511. containersByLabel, keys, err := groupContainerByLabel(containers, projectLabel)
  512. if err != nil {
  513. return nil, err
  514. }
  515. var projects []compose.Stack
  516. for _, project := range keys {
  517. projects = append(projects, compose.Stack{
  518. ID: project,
  519. Name: project,
  520. Status: combinedStatus(containerToState(containersByLabel[project])),
  521. })
  522. }
  523. return projects, nil
  524. }
  525. func containerToState(containers []moby.Container) []string {
  526. statuses := []string{}
  527. for _, c := range containers {
  528. statuses = append(statuses, c.State)
  529. }
  530. return statuses
  531. }
  532. func combinedStatus(statuses []string) string {
  533. nbByStatus := map[string]int{}
  534. keys := []string{}
  535. for _, status := range statuses {
  536. nb, ok := nbByStatus[status]
  537. if !ok {
  538. nb = 0
  539. keys = append(keys, status)
  540. }
  541. nbByStatus[status] = nb + 1
  542. }
  543. sort.Strings(keys)
  544. result := ""
  545. for _, status := range keys {
  546. nb := nbByStatus[status]
  547. if result != "" {
  548. result = result + ", "
  549. }
  550. result = result + fmt.Sprintf("%s(%d)", status, nb)
  551. }
  552. return result
  553. }
  554. func (s *composeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
  555. switch format {
  556. case "json":
  557. return json.MarshalIndent(project, "", " ")
  558. case "yaml":
  559. return yaml.Marshal(project)
  560. default:
  561. return nil, fmt.Errorf("unsupported format %q", format)
  562. }
  563. }
  564. func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
  565. hash, err := jsonHash(s)
  566. if err != nil {
  567. return nil, nil, nil, err
  568. }
  569. // TODO: change oneoffLabel value for containers started with `docker compose run`
  570. labels := map[string]string{
  571. projectLabel: p.Name,
  572. serviceLabel: s.Name,
  573. versionLabel: ComposeVersion,
  574. oneoffLabel: "False",
  575. configHashLabel: hash,
  576. workingDirLabel: p.WorkingDir,
  577. configFilesLabel: strings.Join(p.ComposeFiles, ","),
  578. containerNumberLabel: strconv.Itoa(number),
  579. }
  580. var (
  581. runCmd strslice.StrSlice
  582. entrypoint strslice.StrSlice
  583. )
  584. if len(s.Command) > 0 {
  585. runCmd = strslice.StrSlice(s.Command)
  586. }
  587. if len(s.Entrypoint) > 0 {
  588. entrypoint = strslice.StrSlice(s.Entrypoint)
  589. }
  590. image := s.Image
  591. if s.Image == "" {
  592. image = fmt.Sprintf("%s_%s", p.Name, s.Name)
  593. }
  594. var (
  595. tty = s.Tty
  596. stdinOpen = s.StdinOpen
  597. attachStdin = false
  598. )
  599. containerConfig := container.Config{
  600. Hostname: s.Hostname,
  601. Domainname: s.DomainName,
  602. User: s.User,
  603. ExposedPorts: buildContainerPorts(s),
  604. Tty: tty,
  605. OpenStdin: stdinOpen,
  606. StdinOnce: true,
  607. AttachStdin: attachStdin,
  608. AttachStderr: true,
  609. AttachStdout: true,
  610. Cmd: runCmd,
  611. Image: image,
  612. WorkingDir: s.WorkingDir,
  613. Entrypoint: entrypoint,
  614. NetworkDisabled: s.NetworkMode == "disabled",
  615. MacAddress: s.MacAddress,
  616. Labels: labels,
  617. StopSignal: s.StopSignal,
  618. Env: toMobyEnv(s.Environment),
  619. Healthcheck: toMobyHealthCheck(s.HealthCheck),
  620. // Volumes: // FIXME unclear to me the overlap with HostConfig.Mounts
  621. StopTimeout: toSeconds(s.StopGracePeriod),
  622. }
  623. mountOptions := buildContainerMountOptions(p, s, inherit)
  624. bindings := buildContainerBindingOptions(s)
  625. networkMode := getNetworkMode(p, s)
  626. hostConfig := container.HostConfig{
  627. Mounts: mountOptions,
  628. CapAdd: strslice.StrSlice(s.CapAdd),
  629. CapDrop: strslice.StrSlice(s.CapDrop),
  630. NetworkMode: networkMode,
  631. Init: s.Init,
  632. ReadonlyRootfs: s.ReadOnly,
  633. // ShmSize: , TODO
  634. Sysctls: s.Sysctls,
  635. PortBindings: bindings,
  636. }
  637. networkConfig := buildDefaultNetworkConfig(s, networkMode)
  638. return &containerConfig, &hostConfig, networkConfig, nil
  639. }
  640. func buildContainerPorts(s types.ServiceConfig) nat.PortSet {
  641. ports := nat.PortSet{}
  642. for _, p := range s.Ports {
  643. p := nat.Port(fmt.Sprintf("%d/%s", p.Target, p.Protocol))
  644. ports[p] = struct{}{}
  645. }
  646. return ports
  647. }
  648. func buildContainerBindingOptions(s types.ServiceConfig) nat.PortMap {
  649. bindings := nat.PortMap{}
  650. for _, port := range s.Ports {
  651. p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
  652. bind := []nat.PortBinding{}
  653. binding := nat.PortBinding{}
  654. if port.Published > 0 {
  655. binding.HostPort = fmt.Sprint(port.Published)
  656. }
  657. bind = append(bind, binding)
  658. bindings[p] = bind
  659. }
  660. return bindings
  661. }
  662. func buildContainerMountOptions(p *types.Project, s types.ServiceConfig, inherit *moby.Container) []mount.Mount {
  663. mounts := []mount.Mount{}
  664. var inherited []string
  665. if inherit != nil {
  666. for _, m := range inherit.Mounts {
  667. if m.Type == "tmpfs" {
  668. continue
  669. }
  670. src := m.Source
  671. if m.Type == "volume" {
  672. src = m.Name
  673. }
  674. mounts = append(mounts, mount.Mount{
  675. Type: m.Type,
  676. Source: src,
  677. Target: m.Destination,
  678. ReadOnly: !m.RW,
  679. })
  680. inherited = append(inherited, m.Destination)
  681. }
  682. }
  683. for _, v := range s.Volumes {
  684. if contains(inherited, v.Target) {
  685. continue
  686. }
  687. source := v.Source
  688. if v.Type == "bind" && !filepath.IsAbs(source) {
  689. // FIXME handle ~/
  690. source = filepath.Join(p.WorkingDir, source)
  691. }
  692. mounts = append(mounts, mount.Mount{
  693. Type: mount.Type(v.Type),
  694. Source: source,
  695. Target: v.Target,
  696. ReadOnly: v.ReadOnly,
  697. Consistency: mount.Consistency(v.Consistency),
  698. BindOptions: buildBindOption(v.Bind),
  699. VolumeOptions: buildVolumeOptions(v.Volume),
  700. TmpfsOptions: buildTmpfsOptions(v.Tmpfs),
  701. })
  702. }
  703. return mounts
  704. }
  705. func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
  706. if bind == nil {
  707. return nil
  708. }
  709. return &mount.BindOptions{
  710. Propagation: mount.Propagation(bind.Propagation),
  711. // NonRecursive: false, FIXME missing from model ?
  712. }
  713. }
  714. func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {
  715. if vol == nil {
  716. return nil
  717. }
  718. return &mount.VolumeOptions{
  719. NoCopy: vol.NoCopy,
  720. // Labels: , // FIXME missing from model ?
  721. // DriverConfig: , // FIXME missing from model ?
  722. }
  723. }
  724. func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
  725. if tmpfs == nil {
  726. return nil
  727. }
  728. return &mount.TmpfsOptions{
  729. SizeBytes: tmpfs.Size,
  730. // Mode: , // FIXME missing from model ?
  731. }
  732. }
  733. func buildDefaultNetworkConfig(s types.ServiceConfig, networkMode container.NetworkMode) *network.NetworkingConfig {
  734. config := map[string]*network.EndpointSettings{}
  735. net := string(networkMode)
  736. config[net] = &network.EndpointSettings{
  737. Aliases: getAliases(s, s.Networks[net]),
  738. }
  739. return &network.NetworkingConfig{
  740. EndpointsConfig: config,
  741. }
  742. }
  743. func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
  744. aliases := []string{s.Name}
  745. if c != nil {
  746. aliases = append(aliases, c.Aliases...)
  747. }
  748. return aliases
  749. }
  750. func getNetworkMode(p *types.Project, service types.ServiceConfig) container.NetworkMode {
  751. mode := service.NetworkMode
  752. if mode == "" {
  753. if len(p.Networks) > 0 {
  754. for name := range getNetworksForService(service) {
  755. return container.NetworkMode(p.Networks[name].Name)
  756. }
  757. }
  758. return container.NetworkMode("none")
  759. }
  760. // FIXME incomplete implementation
  761. if strings.HasPrefix(mode, "service:") {
  762. panic("Not yet implemented")
  763. }
  764. if strings.HasPrefix(mode, "container:") {
  765. panic("Not yet implemented")
  766. }
  767. return container.NetworkMode(mode)
  768. }
  769. func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
  770. if len(s.Networks) > 0 {
  771. return s.Networks
  772. }
  773. return map[string]*types.ServiceNetworkConfig{"default": nil}
  774. }
  775. func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
  776. _, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
  777. if err != nil {
  778. if errdefs.IsNotFound(err) {
  779. if n.External.External {
  780. return fmt.Errorf("network %s declared as external, but could not be found", n.Name)
  781. }
  782. createOpts := moby.NetworkCreate{
  783. // TODO NameSpace Labels
  784. Labels: n.Labels,
  785. Driver: n.Driver,
  786. Options: n.DriverOpts,
  787. Internal: n.Internal,
  788. Attachable: n.Attachable,
  789. }
  790. if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
  791. createOpts.IPAM = &network.IPAM{}
  792. }
  793. if n.Ipam.Driver != "" {
  794. createOpts.IPAM.Driver = n.Ipam.Driver
  795. }
  796. for _, ipamConfig := range n.Ipam.Config {
  797. config := network.IPAMConfig{
  798. Subnet: ipamConfig.Subnet,
  799. }
  800. createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
  801. }
  802. networkEventName := fmt.Sprintf("Network %q", n.Name)
  803. w := progress.ContextWriter(ctx)
  804. w.Event(progress.CreatingEvent(networkEventName))
  805. if _, err := s.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil {
  806. w.Event(progress.ErrorEvent(networkEventName))
  807. return errors.Wrapf(err, "failed to create network %s", n.Name)
  808. }
  809. w.Event(progress.CreatedEvent(networkEventName))
  810. return nil
  811. }
  812. return err
  813. }
  814. return nil
  815. }
  816. func (s *composeService) ensureNetworkDown(ctx context.Context, networkID string, networkName string) error {
  817. w := progress.ContextWriter(ctx)
  818. eventName := fmt.Sprintf("Network %q", networkName)
  819. w.Event(progress.RemovingEvent(eventName))
  820. if err := s.apiClient.NetworkRemove(ctx, networkID); err != nil {
  821. w.Event(progress.ErrorEvent(eventName))
  822. return errors.Wrapf(err, fmt.Sprintf("failed to create network %s", networkID))
  823. }
  824. w.Event(progress.RemovedEvent(eventName))
  825. return nil
  826. }
  827. func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig) error {
  828. // TODO could identify volume by label vs name
  829. _, err := s.apiClient.VolumeInspect(ctx, volume.Name)
  830. if err != nil {
  831. if errdefs.IsNotFound(err) {
  832. eventName := fmt.Sprintf("Volume %q", volume.Name)
  833. w := progress.ContextWriter(ctx)
  834. w.Event(progress.CreatingEvent(eventName))
  835. // TODO we miss support for driver_opts and labels
  836. _, err := s.apiClient.VolumeCreate(ctx, mobyvolume.VolumeCreateBody{
  837. Labels: volume.Labels,
  838. Name: volume.Name,
  839. Driver: volume.Driver,
  840. DriverOpts: volume.DriverOpts,
  841. })
  842. if err != nil {
  843. w.Event(progress.ErrorEvent(eventName))
  844. return err
  845. }
  846. w.Event(progress.CreatedEvent(eventName))
  847. }
  848. return err
  849. }
  850. return nil
  851. }