1
0

compose.go 27 KB

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