loader.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  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. "errors"
  17. "os"
  18. "strings"
  19. "github.com/compose-spec/compose-go/v2/cli"
  20. "github.com/compose-spec/compose-go/v2/loader"
  21. "github.com/compose-spec/compose-go/v2/types"
  22. "github.com/docker/compose/v5/pkg/api"
  23. "github.com/docker/compose/v5/pkg/remote"
  24. "github.com/docker/compose/v5/pkg/utils"
  25. )
  26. // LoadProject implements api.Compose.LoadProject
  27. // It loads and validates a Compose project from configuration files.
  28. func (s *composeService) LoadProject(ctx context.Context, options api.ProjectLoadOptions) (*types.Project, error) {
  29. // Setup remote loaders (Git, OCI)
  30. remoteLoaders := s.createRemoteLoaders(options)
  31. projectOptions, err := s.buildProjectOptions(options, remoteLoaders)
  32. if err != nil {
  33. return nil, err
  34. }
  35. // Register all user-provided listeners (e.g., for metrics collection)
  36. for _, listener := range options.LoadListeners {
  37. if listener != nil {
  38. projectOptions.WithListeners(listener)
  39. }
  40. }
  41. if options.Compatibility || utils.StringToBool(projectOptions.Environment[api.ComposeCompatibility]) {
  42. api.Separator = "_"
  43. }
  44. project, err := projectOptions.LoadProject(ctx)
  45. if err != nil {
  46. return nil, err
  47. }
  48. // Post-processing: service selection, environment resolution, etc.
  49. project, err = s.postProcessProject(project, options)
  50. if err != nil {
  51. return nil, err
  52. }
  53. return project, nil
  54. }
  55. // createRemoteLoaders creates Git and OCI remote loaders if not in offline mode
  56. func (s *composeService) createRemoteLoaders(options api.ProjectLoadOptions) []loader.ResourceLoader {
  57. if options.Offline {
  58. return nil
  59. }
  60. git := remote.NewGitRemoteLoader(s.dockerCli, options.Offline)
  61. oci := remote.NewOCIRemoteLoader(s.dockerCli, options.Offline, options.OCI)
  62. return []loader.ResourceLoader{git, oci}
  63. }
  64. // buildProjectOptions constructs compose-go ProjectOptions from API options
  65. func (s *composeService) buildProjectOptions(options api.ProjectLoadOptions, remoteLoaders []loader.ResourceLoader) (*cli.ProjectOptions, error) {
  66. opts := []cli.ProjectOptionsFn{
  67. cli.WithWorkingDirectory(options.WorkingDir),
  68. cli.WithOsEnv,
  69. }
  70. // Add PWD if not present
  71. if _, present := os.LookupEnv("PWD"); !present {
  72. if pwd, err := os.Getwd(); err == nil {
  73. opts = append(opts, cli.WithEnv([]string{"PWD=" + pwd}))
  74. }
  75. }
  76. // Add remote loaders
  77. for _, r := range remoteLoaders {
  78. opts = append(opts, cli.WithResourceLoader(r))
  79. }
  80. opts = append(opts,
  81. // Load PWD/.env if present and no explicit --env-file has been set
  82. cli.WithEnvFiles(options.EnvFiles...),
  83. // read dot env file to populate project environment
  84. cli.WithDotEnv,
  85. // get compose file path set by COMPOSE_FILE
  86. cli.WithConfigFileEnv,
  87. // if none was selected, get default compose.yaml file from current dir or parent folder
  88. cli.WithDefaultConfigPath,
  89. // .. and then, a project directory != PWD maybe has been set so let's load .env file
  90. cli.WithEnvFiles(options.EnvFiles...), //nolint:gocritic // intentionally applying cli.WithEnvFiles twice.
  91. cli.WithDotEnv, //nolint:gocritic // intentionally applying cli.WithDotEnv twice.
  92. // eventually COMPOSE_PROFILES should have been set
  93. cli.WithDefaultProfiles(options.Profiles...),
  94. cli.WithName(options.ProjectName),
  95. )
  96. return cli.NewProjectOptions(options.ConfigPaths, append(options.ProjectOptionsFns, opts...)...)
  97. }
  98. // postProcessProject applies post-loading transformations to the project
  99. func (s *composeService) postProcessProject(project *types.Project, options api.ProjectLoadOptions) (*types.Project, error) {
  100. if project.Name == "" {
  101. return nil, errors.New("project name can't be empty. Use ProjectName option to set a valid name")
  102. }
  103. project, err := project.WithServicesEnabled(options.Services...)
  104. if err != nil {
  105. return nil, err
  106. }
  107. // Add custom labels
  108. for name, s := range project.Services {
  109. s.CustomLabels = map[string]string{
  110. api.ProjectLabel: project.Name,
  111. api.ServiceLabel: name,
  112. api.VersionLabel: api.ComposeVersion,
  113. api.WorkingDirLabel: project.WorkingDir,
  114. api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
  115. api.OneoffLabel: "False",
  116. }
  117. if len(options.EnvFiles) != 0 {
  118. s.CustomLabels[api.EnvironmentFileLabel] = strings.Join(options.EnvFiles, ",")
  119. }
  120. project.Services[name] = s
  121. }
  122. project, err = project.WithSelectedServices(options.Services)
  123. if err != nil {
  124. return nil, err
  125. }
  126. // Remove unnecessary resources if not All
  127. if !options.All {
  128. project = project.WithoutUnnecessaryResources()
  129. }
  130. return project, nil
  131. }