loader_test.go 9.1 KB

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