loader_test.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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. "os"
  16. "path/filepath"
  17. "testing"
  18. "github.com/compose-spec/compose-go/v2/cli"
  19. "gotest.tools/v3/assert"
  20. is "gotest.tools/v3/assert/cmp"
  21. "github.com/docker/compose/v5/pkg/api"
  22. )
  23. func TestLoadProject_Basic(t *testing.T) {
  24. // Create a temporary compose file
  25. tmpDir := t.TempDir()
  26. composeFile := filepath.Join(tmpDir, "compose.yaml")
  27. composeContent := `
  28. name: test-project
  29. services:
  30. web:
  31. image: nginx:latest
  32. ports:
  33. - "8080:80"
  34. db:
  35. image: postgres:latest
  36. environment:
  37. POSTGRES_PASSWORD: secret
  38. `
  39. err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
  40. assert.NilError(t, err)
  41. service, err := NewComposeService(nil)
  42. assert.NilError(t, err)
  43. // Load the project
  44. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  45. ConfigPaths: []string{composeFile},
  46. })
  47. // Assertions
  48. assert.NilError(t, err)
  49. assert.Equal(t, "test-project", project.Name)
  50. assert.Assert(t, is.Len(project.Services, 2))
  51. assert.Check(t, is.Contains(project.Services, "web"))
  52. assert.Check(t, is.Contains(project.Services, "db"))
  53. // Check labels were applied
  54. webService := project.Services["web"]
  55. assert.Equal(t, "test-project", webService.CustomLabels[api.ProjectLabel])
  56. assert.Equal(t, "web", webService.CustomLabels[api.ServiceLabel])
  57. }
  58. func TestLoadProject_WithEnvironmentResolution(t *testing.T) {
  59. tmpDir := t.TempDir()
  60. composeFile := filepath.Join(tmpDir, "compose.yaml")
  61. composeContent := `
  62. services:
  63. app:
  64. image: myapp:latest
  65. environment:
  66. - TEST_VAR=${TEST_VAR}
  67. - LITERAL_VAR=literal_value
  68. `
  69. err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
  70. assert.NilError(t, err)
  71. // Set environment variable
  72. t.Setenv("TEST_VAR", "resolved_value")
  73. service, err := NewComposeService(nil)
  74. assert.NilError(t, err)
  75. // Test with environment resolution (default)
  76. t.Run("WithResolution", func(t *testing.T) {
  77. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  78. ConfigPaths: []string{composeFile},
  79. })
  80. assert.NilError(t, err)
  81. appService := project.Services["app"]
  82. // Environment should be resolved
  83. assert.Assert(t, appService.Environment["TEST_VAR"] != nil)
  84. assert.Equal(t, "resolved_value", *appService.Environment["TEST_VAR"])
  85. assert.Assert(t, appService.Environment["LITERAL_VAR"] != nil)
  86. assert.Equal(t, "literal_value", *appService.Environment["LITERAL_VAR"])
  87. })
  88. // Test without environment resolution
  89. t.Run("WithoutResolution", func(t *testing.T) {
  90. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  91. ConfigPaths: []string{composeFile},
  92. ProjectOptionsFns: []cli.ProjectOptionsFn{cli.WithoutEnvironmentResolution},
  93. })
  94. assert.NilError(t, err)
  95. appService := project.Services["app"]
  96. // Environment should NOT be resolved, keeping raw values
  97. // Note: This depends on compose-go behavior, which may still have some resolution
  98. assert.Assert(t, appService.Environment != nil)
  99. })
  100. }
  101. func TestLoadProject_ServiceSelection(t *testing.T) {
  102. tmpDir := t.TempDir()
  103. composeFile := filepath.Join(tmpDir, "compose.yaml")
  104. composeContent := `
  105. services:
  106. web:
  107. image: nginx:latest
  108. db:
  109. image: postgres:latest
  110. cache:
  111. image: redis:latest
  112. `
  113. err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
  114. assert.NilError(t, err)
  115. service, err := NewComposeService(nil)
  116. assert.NilError(t, err)
  117. // Load only specific services
  118. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  119. ConfigPaths: []string{composeFile},
  120. Services: []string{"web", "db"},
  121. })
  122. assert.NilError(t, err)
  123. assert.Check(t, is.Len(project.Services, 2))
  124. assert.Check(t, is.Contains(project.Services, "web"))
  125. assert.Check(t, is.Contains(project.Services, "db"))
  126. assert.Check(t, !is.Contains(project.Services, "cache")().Success())
  127. }
  128. func TestLoadProject_WithProfiles(t *testing.T) {
  129. tmpDir := t.TempDir()
  130. composeFile := filepath.Join(tmpDir, "compose.yaml")
  131. composeContent := `
  132. services:
  133. web:
  134. image: nginx:latest
  135. debug:
  136. image: busybox:latest
  137. profiles: ["debug"]
  138. `
  139. err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
  140. assert.NilError(t, err)
  141. service, err := NewComposeService(nil)
  142. assert.NilError(t, err)
  143. // Without debug profile
  144. t.Run("WithoutProfile", func(t *testing.T) {
  145. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  146. ConfigPaths: []string{composeFile},
  147. })
  148. assert.NilError(t, err)
  149. assert.Check(t, is.Len(project.Services, 1))
  150. assert.Check(t, is.Contains(project.Services, "web"))
  151. })
  152. // With debug profile
  153. t.Run("WithProfile", func(t *testing.T) {
  154. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  155. ConfigPaths: []string{composeFile},
  156. Profiles: []string{"debug"},
  157. })
  158. assert.NilError(t, err)
  159. assert.Check(t, is.Len(project.Services, 2))
  160. assert.Check(t, is.Contains(project.Services, "web"))
  161. assert.Check(t, is.Contains(project.Services, "debug"))
  162. })
  163. }
  164. func TestLoadProject_WithLoadListeners(t *testing.T) {
  165. tmpDir := t.TempDir()
  166. composeFile := filepath.Join(tmpDir, "compose.yaml")
  167. composeContent := `
  168. services:
  169. web:
  170. image: nginx:latest
  171. `
  172. err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
  173. assert.NilError(t, err)
  174. service, err := NewComposeService(nil)
  175. assert.NilError(t, err)
  176. // Track events received
  177. var events []string
  178. listener := func(event string, metadata map[string]any) {
  179. events = append(events, event)
  180. }
  181. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  182. ConfigPaths: []string{composeFile},
  183. LoadListeners: []api.LoadListener{listener},
  184. })
  185. assert.NilError(t, err)
  186. assert.Assert(t, project != nil)
  187. // Listeners should have been called (exact events depend on compose-go implementation)
  188. // The slice itself is always initialized (non-nil), even if empty
  189. _ = events // events may or may not have entries depending on compose-go behavior
  190. }
  191. func TestLoadProject_ProjectNameInference(t *testing.T) {
  192. tmpDir := t.TempDir()
  193. composeFile := filepath.Join(tmpDir, "compose.yaml")
  194. composeContent := `
  195. services:
  196. web:
  197. image: nginx:latest
  198. `
  199. err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
  200. assert.NilError(t, err)
  201. service, err := NewComposeService(nil)
  202. assert.NilError(t, err)
  203. // Without explicit project name
  204. t.Run("InferredName", func(t *testing.T) {
  205. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  206. ConfigPaths: []string{composeFile},
  207. })
  208. assert.NilError(t, err)
  209. // Project name should be inferred from directory
  210. assert.Assert(t, project.Name != "")
  211. })
  212. // With explicit project name
  213. t.Run("ExplicitName", func(t *testing.T) {
  214. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  215. ConfigPaths: []string{composeFile},
  216. ProjectName: "my-custom-project",
  217. })
  218. assert.NilError(t, err)
  219. assert.Equal(t, "my-custom-project", project.Name)
  220. })
  221. }
  222. func TestLoadProject_Compatibility(t *testing.T) {
  223. tmpDir := t.TempDir()
  224. composeFile := filepath.Join(tmpDir, "compose.yaml")
  225. composeContent := `
  226. services:
  227. web:
  228. image: nginx:latest
  229. `
  230. err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
  231. assert.NilError(t, err)
  232. service, err := NewComposeService(nil)
  233. assert.NilError(t, err)
  234. // With compatibility mode
  235. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  236. ConfigPaths: []string{composeFile},
  237. Compatibility: true,
  238. })
  239. assert.NilError(t, err)
  240. assert.Assert(t, project != nil)
  241. // In compatibility mode, separator should be "_"
  242. assert.Equal(t, "_", api.Separator)
  243. // Reset separator
  244. api.Separator = "-"
  245. }
  246. func TestLoadProject_InvalidComposeFile(t *testing.T) {
  247. tmpDir := t.TempDir()
  248. composeFile := filepath.Join(tmpDir, "compose.yaml")
  249. composeContent := `
  250. this is not valid yaml: [[[
  251. `
  252. err := os.WriteFile(composeFile, []byte(composeContent), 0o644)
  253. assert.NilError(t, err)
  254. service, err := NewComposeService(nil)
  255. assert.NilError(t, err)
  256. // Should return an error for invalid YAML
  257. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  258. ConfigPaths: []string{composeFile},
  259. })
  260. assert.Assert(t, err != nil)
  261. assert.Assert(t, project == nil)
  262. }
  263. func TestLoadProject_MissingComposeFile(t *testing.T) {
  264. service, err := NewComposeService(nil)
  265. assert.NilError(t, err)
  266. // Should return an error for missing file
  267. project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
  268. ConfigPaths: []string{"/nonexistent/compose.yaml"},
  269. })
  270. assert.Assert(t, err != nil)
  271. assert.Assert(t, project == nil)
  272. }