generate.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /*
  2. Copyright 2023 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. "maps"
  18. "slices"
  19. "strings"
  20. "github.com/compose-spec/compose-go/v2/types"
  21. "github.com/moby/moby/api/types/container"
  22. "github.com/moby/moby/api/types/mount"
  23. "github.com/moby/moby/api/types/network"
  24. "github.com/moby/moby/client"
  25. "github.com/docker/compose/v5/pkg/api"
  26. )
  27. func (s *composeService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) {
  28. res, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
  29. Filters: make(client.Filters).Add("name", options.Containers...),
  30. All: true,
  31. })
  32. if err != nil {
  33. return nil, err
  34. }
  35. containers := res.Items
  36. containersByIds, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
  37. Filters: make(client.Filters).Add("id", options.Containers...),
  38. All: true,
  39. })
  40. if err != nil {
  41. return nil, err
  42. }
  43. for _, ctr := range containersByIds.Items {
  44. if !slices.ContainsFunc(containers, func(summary container.Summary) bool {
  45. return summary.ID == ctr.ID
  46. }) {
  47. containers = append(containers, ctr)
  48. }
  49. }
  50. if len(containers) == 0 {
  51. return nil, fmt.Errorf("no container(s) found with the following name(s): %s", strings.Join(options.Containers, ","))
  52. }
  53. return s.createProjectFromContainers(containers, options.ProjectName)
  54. }
  55. func (s *composeService) createProjectFromContainers(containers []container.Summary, projectName string) (*types.Project, error) {
  56. project := &types.Project{}
  57. services := types.Services{}
  58. networks := types.Networks{}
  59. volumes := types.Volumes{}
  60. secrets := types.Secrets{}
  61. if projectName != "" {
  62. project.Name = projectName
  63. }
  64. for _, c := range containers {
  65. // if the container is from a previous Compose application, use the existing service name
  66. serviceLabel, ok := c.Labels[api.ServiceLabel]
  67. if !ok {
  68. serviceLabel = getCanonicalContainerName(c)
  69. }
  70. service, ok := services[serviceLabel]
  71. if !ok {
  72. service = types.ServiceConfig{
  73. Name: serviceLabel,
  74. Image: c.Image,
  75. Labels: c.Labels,
  76. }
  77. }
  78. service.Scale = increment(service.Scale)
  79. inspect, err := s.apiClient().ContainerInspect(context.Background(), c.ID, client.ContainerInspectOptions{})
  80. if err != nil {
  81. services[serviceLabel] = service
  82. continue
  83. }
  84. s.extractComposeConfiguration(&service, inspect.Container, volumes, secrets, networks)
  85. service.Labels = cleanDockerPreviousLabels(service.Labels)
  86. services[serviceLabel] = service
  87. }
  88. project.Services = services
  89. project.Networks = networks
  90. project.Volumes = volumes
  91. project.Secrets = secrets
  92. return project, nil
  93. }
  94. func (s *composeService) extractComposeConfiguration(service *types.ServiceConfig, inspect container.InspectResponse, volumes types.Volumes, secrets types.Secrets, networks types.Networks) {
  95. service.Environment = types.NewMappingWithEquals(inspect.Config.Env)
  96. if inspect.Config.Healthcheck != nil {
  97. healthConfig := inspect.Config.Healthcheck
  98. service.HealthCheck = s.toComposeHealthCheck(healthConfig)
  99. }
  100. if len(inspect.Mounts) > 0 {
  101. detectedVolumes, volumeConfigs, detectedSecrets, secretsConfigs := s.toComposeVolumes(inspect.Mounts)
  102. service.Volumes = append(service.Volumes, volumeConfigs...)
  103. service.Secrets = append(service.Secrets, secretsConfigs...)
  104. maps.Copy(volumes, detectedVolumes)
  105. maps.Copy(secrets, detectedSecrets)
  106. }
  107. if len(inspect.NetworkSettings.Networks) > 0 {
  108. detectedNetworks, networkConfigs := s.toComposeNetwork(inspect.NetworkSettings.Networks)
  109. service.Networks = networkConfigs
  110. maps.Copy(networks, detectedNetworks)
  111. }
  112. if len(inspect.HostConfig.PortBindings) > 0 {
  113. for key, portBindings := range inspect.HostConfig.PortBindings {
  114. for _, portBinding := range portBindings {
  115. service.Ports = append(service.Ports, types.ServicePortConfig{
  116. Target: uint32(key.Num()),
  117. Published: portBinding.HostPort,
  118. Protocol: string(key.Proto()),
  119. HostIP: portBinding.HostIP.String(),
  120. })
  121. }
  122. }
  123. }
  124. }
  125. func (s *composeService) toComposeHealthCheck(healthConfig *container.HealthConfig) *types.HealthCheckConfig {
  126. var healthCheck types.HealthCheckConfig
  127. healthCheck.Test = healthConfig.Test
  128. if healthConfig.Timeout != 0 {
  129. timeout := types.Duration(healthConfig.Timeout)
  130. healthCheck.Timeout = &timeout
  131. }
  132. if healthConfig.Interval != 0 {
  133. interval := types.Duration(healthConfig.Interval)
  134. healthCheck.Interval = &interval
  135. }
  136. if healthConfig.StartPeriod != 0 {
  137. startPeriod := types.Duration(healthConfig.StartPeriod)
  138. healthCheck.StartPeriod = &startPeriod
  139. }
  140. if healthConfig.StartInterval != 0 {
  141. startInterval := types.Duration(healthConfig.StartInterval)
  142. healthCheck.StartInterval = &startInterval
  143. }
  144. if healthConfig.Retries != 0 {
  145. retries := uint64(healthConfig.Retries)
  146. healthCheck.Retries = &retries
  147. }
  148. return &healthCheck
  149. }
  150. func (s *composeService) toComposeVolumes(volumes []container.MountPoint) (map[string]types.VolumeConfig,
  151. []types.ServiceVolumeConfig, map[string]types.SecretConfig, []types.ServiceSecretConfig,
  152. ) {
  153. volumeConfigs := make(map[string]types.VolumeConfig)
  154. secretConfigs := make(map[string]types.SecretConfig)
  155. var serviceVolumeConfigs []types.ServiceVolumeConfig
  156. var serviceSecretConfigs []types.ServiceSecretConfig
  157. for _, volume := range volumes {
  158. serviceVC := types.ServiceVolumeConfig{
  159. Type: string(volume.Type),
  160. Source: volume.Source,
  161. Target: volume.Destination,
  162. ReadOnly: !volume.RW,
  163. }
  164. switch volume.Type {
  165. case mount.TypeVolume:
  166. serviceVC.Source = volume.Name
  167. vol := types.VolumeConfig{}
  168. if volume.Driver != "local" {
  169. vol.Driver = volume.Driver
  170. vol.Name = volume.Name
  171. }
  172. volumeConfigs[volume.Name] = vol
  173. serviceVolumeConfigs = append(serviceVolumeConfigs, serviceVC)
  174. case mount.TypeBind:
  175. if strings.HasPrefix(volume.Destination, "/run/secrets") {
  176. destination := strings.Split(volume.Destination, "/")
  177. secret := types.SecretConfig{
  178. Name: destination[len(destination)-1],
  179. File: strings.TrimPrefix(volume.Source, "/host_mnt"),
  180. }
  181. secretConfigs[secret.Name] = secret
  182. serviceSecretConfigs = append(serviceSecretConfigs, types.ServiceSecretConfig{
  183. Source: secret.Name,
  184. Target: volume.Destination,
  185. })
  186. } else {
  187. serviceVolumeConfigs = append(serviceVolumeConfigs, serviceVC)
  188. }
  189. }
  190. }
  191. return volumeConfigs, serviceVolumeConfigs, secretConfigs, serviceSecretConfigs
  192. }
  193. func (s *composeService) toComposeNetwork(networks map[string]*network.EndpointSettings) (map[string]types.NetworkConfig, map[string]*types.ServiceNetworkConfig) {
  194. networkConfigs := make(map[string]types.NetworkConfig)
  195. serviceNetworkConfigs := make(map[string]*types.ServiceNetworkConfig)
  196. for name, net := range networks {
  197. inspect, err := s.apiClient().NetworkInspect(context.Background(), name, client.NetworkInspectOptions{})
  198. if err != nil {
  199. networkConfigs[name] = types.NetworkConfig{}
  200. } else {
  201. networkConfigs[name] = types.NetworkConfig{
  202. Internal: inspect.Network.Internal,
  203. }
  204. }
  205. serviceNetworkConfigs[name] = &types.ServiceNetworkConfig{
  206. Aliases: net.Aliases,
  207. }
  208. }
  209. return networkConfigs, serviceNetworkConfigs
  210. }
  211. func cleanDockerPreviousLabels(labels types.Labels) types.Labels {
  212. cleanedLabels := types.Labels{}
  213. for key, value := range labels {
  214. if !strings.HasPrefix(key, "com.docker.compose.") && !strings.HasPrefix(key, "desktop.docker.io") {
  215. cleanedLabels[key] = value
  216. }
  217. }
  218. return cleanedLabels
  219. }