convert.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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 bridge
  14. import (
  15. "context"
  16. "fmt"
  17. "io"
  18. "os"
  19. "os/user"
  20. "path/filepath"
  21. "strconv"
  22. "github.com/compose-spec/compose-go/v2/types"
  23. cerrdefs "github.com/containerd/errdefs"
  24. "github.com/docker/cli/cli/command"
  25. cli "github.com/docker/cli/cli/command/container"
  26. "github.com/docker/compose/v2/pkg/api"
  27. "github.com/docker/compose/v2/pkg/utils"
  28. "github.com/docker/docker/api/types/container"
  29. "github.com/docker/docker/api/types/image"
  30. "github.com/docker/docker/api/types/network"
  31. "github.com/docker/docker/pkg/jsonmessage"
  32. "github.com/docker/go-connections/nat"
  33. "gopkg.in/yaml.v3"
  34. )
  35. type ConvertOptions struct {
  36. Output string
  37. Templates string
  38. Transformations []string
  39. }
  40. func Convert(ctx context.Context, dockerCli command.Cli, project *types.Project, opts ConvertOptions) error {
  41. if len(opts.Transformations) == 0 {
  42. opts.Transformations = []string{DefaultTransformerImage}
  43. }
  44. // Load image references, secrets and configs, also expose ports
  45. project, err := LoadAdditionalResources(ctx, dockerCli, project)
  46. if err != nil {
  47. return err
  48. }
  49. // for user to rely on compose.yaml attribute names, not go struct ones, we marshall back into YAML
  50. raw, err := project.MarshalYAML(types.WithSecretContent)
  51. // Marshall to YAML
  52. if err != nil {
  53. return fmt.Errorf("cannot render project into yaml: %w", err)
  54. }
  55. var model map[string]any
  56. err = yaml.Unmarshal(raw, &model)
  57. if err != nil {
  58. return fmt.Errorf("cannot render project into yaml: %w", err)
  59. }
  60. if opts.Output != "" {
  61. _ = os.RemoveAll(opts.Output)
  62. err := os.MkdirAll(opts.Output, 0o744)
  63. if err != nil && !os.IsExist(err) {
  64. return fmt.Errorf("cannot create output folder: %w", err)
  65. }
  66. }
  67. // Run Transformers images
  68. return convert(ctx, dockerCli, model, opts)
  69. }
  70. func convert(ctx context.Context, dockerCli command.Cli, model map[string]any, opts ConvertOptions) error {
  71. raw, err := yaml.Marshal(model)
  72. if err != nil {
  73. return err
  74. }
  75. dir := os.TempDir()
  76. composeYaml := filepath.Join(dir, "compose.yaml")
  77. err = os.WriteFile(composeYaml, raw, 0o600)
  78. if err != nil {
  79. return err
  80. }
  81. out, err := filepath.Abs(opts.Output)
  82. if err != nil {
  83. return err
  84. }
  85. binds := []string{
  86. fmt.Sprintf("%s:%s", dir, "/in"),
  87. fmt.Sprintf("%s:%s", out, "/out"),
  88. }
  89. if opts.Templates != "" {
  90. templateDir, err := filepath.Abs(opts.Templates)
  91. if err != nil {
  92. return err
  93. }
  94. binds = append(binds, fmt.Sprintf("%s:%s", templateDir, "/templates"))
  95. }
  96. for _, transformation := range opts.Transformations {
  97. _, err = inspectWithPull(ctx, dockerCli, transformation)
  98. if err != nil {
  99. return err
  100. }
  101. usr, err := user.Current()
  102. if err != nil {
  103. return err
  104. }
  105. created, err := dockerCli.Client().ContainerCreate(ctx, &container.Config{
  106. Image: transformation,
  107. Env: []string{"LICENSE_AGREEMENT=true"},
  108. User: usr.Uid,
  109. }, &container.HostConfig{
  110. AutoRemove: true,
  111. Binds: binds,
  112. }, &network.NetworkingConfig{}, nil, "")
  113. if err != nil {
  114. return err
  115. }
  116. err = cli.RunStart(ctx, dockerCli, &cli.StartOptions{
  117. Attach: true,
  118. Containers: []string{created.ID},
  119. })
  120. if err != nil {
  121. return err
  122. }
  123. }
  124. return nil
  125. }
  126. // LoadAdditionalResources loads additional resources from the project, such as image references, secrets, configs and exposed ports
  127. func LoadAdditionalResources(ctx context.Context, dockerCLI command.Cli, project *types.Project) (*types.Project, error) {
  128. for name, service := range project.Services {
  129. imageName := api.GetImageNameOrDefault(service, project.Name)
  130. inspect, err := inspectWithPull(ctx, dockerCLI, imageName)
  131. if err != nil {
  132. return nil, err
  133. }
  134. service.Image = imageName
  135. exposed := utils.Set[string]{}
  136. exposed.AddAll(service.Expose...)
  137. for port := range inspect.Config.ExposedPorts {
  138. exposed.Add(nat.Port(port).Port())
  139. }
  140. for _, port := range service.Ports {
  141. exposed.Add(strconv.Itoa(int(port.Target)))
  142. }
  143. service.Expose = exposed.Elements()
  144. project.Services[name] = service
  145. }
  146. for name, secret := range project.Secrets {
  147. f, err := loadFileObject(types.FileObjectConfig(secret))
  148. if err != nil {
  149. return nil, err
  150. }
  151. project.Secrets[name] = types.SecretConfig(f)
  152. }
  153. for name, config := range project.Configs {
  154. f, err := loadFileObject(types.FileObjectConfig(config))
  155. if err != nil {
  156. return nil, err
  157. }
  158. project.Configs[name] = types.ConfigObjConfig(f)
  159. }
  160. return project, nil
  161. }
  162. func loadFileObject(conf types.FileObjectConfig) (types.FileObjectConfig, error) {
  163. if !conf.External {
  164. switch {
  165. case conf.Environment != "":
  166. conf.Content = os.Getenv(conf.Environment)
  167. case conf.File != "":
  168. bytes, err := os.ReadFile(conf.File)
  169. if err != nil {
  170. return conf, err
  171. }
  172. conf.Content = string(bytes)
  173. }
  174. }
  175. return conf, nil
  176. }
  177. func inspectWithPull(ctx context.Context, dockerCli command.Cli, imageName string) (image.InspectResponse, error) {
  178. inspect, err := dockerCli.Client().ImageInspect(ctx, imageName)
  179. if cerrdefs.IsNotFound(err) {
  180. var stream io.ReadCloser
  181. stream, err = dockerCli.Client().ImagePull(ctx, imageName, image.PullOptions{})
  182. if err != nil {
  183. return image.InspectResponse{}, err
  184. }
  185. defer func() { _ = stream.Close() }()
  186. err = jsonmessage.DisplayJSONMessagesToStream(stream, dockerCli.Out(), nil)
  187. if err != nil {
  188. return image.InspectResponse{}, err
  189. }
  190. if inspect, err = dockerCli.Client().ImageInspect(ctx, imageName); err != nil {
  191. return image.InspectResponse{}, err
  192. }
  193. }
  194. return inspect, err
  195. }