loader_test.go 8.8 KB

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