loader.go 4.4 KB

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