convergence_test.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  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. "fmt"
  17. "net/netip"
  18. "strings"
  19. "testing"
  20. "github.com/compose-spec/compose-go/v2/types"
  21. "github.com/docker/cli/cli/config/configfile"
  22. "github.com/google/go-cmp/cmp/cmpopts"
  23. "github.com/moby/moby/api/types/container"
  24. "github.com/moby/moby/api/types/network"
  25. "github.com/moby/moby/client"
  26. "go.uber.org/mock/gomock"
  27. "gotest.tools/v3/assert"
  28. "github.com/docker/compose/v5/pkg/api"
  29. "github.com/docker/compose/v5/pkg/mocks"
  30. )
  31. func TestContainerName(t *testing.T) {
  32. s := types.ServiceConfig{
  33. Name: "testservicename",
  34. ContainerName: "testcontainername",
  35. Scale: intPtr(1),
  36. Deploy: &types.DeployConfig{},
  37. }
  38. ret, err := getScale(s)
  39. assert.NilError(t, err)
  40. assert.Equal(t, ret, *s.Scale)
  41. s.Scale = intPtr(0)
  42. ret, err = getScale(s)
  43. assert.NilError(t, err)
  44. assert.Equal(t, ret, *s.Scale)
  45. s.Scale = intPtr(2)
  46. _, err = getScale(s)
  47. assert.Error(t, err, fmt.Sprintf(doubledContainerNameWarning, s.Name, s.ContainerName))
  48. }
  49. func intPtr(i int) *int {
  50. return &i
  51. }
  52. func TestServiceLinks(t *testing.T) {
  53. const dbContainerName = "/" + testProject + "-db-1"
  54. const webContainerName = "/" + testProject + "-web-1"
  55. s := types.ServiceConfig{
  56. Name: "web",
  57. Scale: intPtr(1),
  58. }
  59. containerListOptions := client.ContainerListOptions{
  60. Filters: projectFilter(testProject).Add("label",
  61. serviceFilter("db"),
  62. oneOffFilter(false),
  63. hasConfigHashLabel(),
  64. ),
  65. All: true,
  66. }
  67. t.Run("service links default", func(t *testing.T) {
  68. mockCtrl := gomock.NewController(t)
  69. defer mockCtrl.Finish()
  70. apiClient := mocks.NewMockAPIClient(mockCtrl)
  71. cli := mocks.NewMockCli(mockCtrl)
  72. tested, err := NewComposeService(cli)
  73. assert.NilError(t, err)
  74. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  75. s.Links = []string{"db"}
  76. c := testContainer("db", dbContainerName, false)
  77. apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return(client.ContainerListResult{
  78. Items: []container.Summary{c},
  79. }, nil)
  80. links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
  81. assert.NilError(t, err)
  82. assert.Equal(t, len(links), 3)
  83. assert.Equal(t, links[0], "testProject-db-1:db")
  84. assert.Equal(t, links[1], "testProject-db-1:db-1")
  85. assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
  86. })
  87. t.Run("service links", func(t *testing.T) {
  88. mockCtrl := gomock.NewController(t)
  89. defer mockCtrl.Finish()
  90. apiClient := mocks.NewMockAPIClient(mockCtrl)
  91. cli := mocks.NewMockCli(mockCtrl)
  92. tested, err := NewComposeService(cli)
  93. assert.NilError(t, err)
  94. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  95. s.Links = []string{"db:db"}
  96. c := testContainer("db", dbContainerName, false)
  97. apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return(client.ContainerListResult{
  98. Items: []container.Summary{c},
  99. }, nil)
  100. links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
  101. assert.NilError(t, err)
  102. assert.Equal(t, len(links), 3)
  103. assert.Equal(t, links[0], "testProject-db-1:db")
  104. assert.Equal(t, links[1], "testProject-db-1:db-1")
  105. assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
  106. })
  107. t.Run("service links name", func(t *testing.T) {
  108. mockCtrl := gomock.NewController(t)
  109. defer mockCtrl.Finish()
  110. apiClient := mocks.NewMockAPIClient(mockCtrl)
  111. cli := mocks.NewMockCli(mockCtrl)
  112. tested, err := NewComposeService(cli)
  113. assert.NilError(t, err)
  114. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  115. s.Links = []string{"db:dbname"}
  116. c := testContainer("db", dbContainerName, false)
  117. apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return(client.ContainerListResult{
  118. Items: []container.Summary{c},
  119. }, nil)
  120. links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
  121. assert.NilError(t, err)
  122. assert.Equal(t, len(links), 3)
  123. assert.Equal(t, links[0], "testProject-db-1:dbname")
  124. assert.Equal(t, links[1], "testProject-db-1:db-1")
  125. assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
  126. })
  127. t.Run("service links external links", func(t *testing.T) {
  128. mockCtrl := gomock.NewController(t)
  129. defer mockCtrl.Finish()
  130. apiClient := mocks.NewMockAPIClient(mockCtrl)
  131. cli := mocks.NewMockCli(mockCtrl)
  132. tested, err := NewComposeService(cli)
  133. assert.NilError(t, err)
  134. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  135. s.Links = []string{"db:dbname"}
  136. s.ExternalLinks = []string{"db1:db2"}
  137. c := testContainer("db", dbContainerName, false)
  138. apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return(client.ContainerListResult{
  139. Items: []container.Summary{c},
  140. }, nil)
  141. links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
  142. assert.NilError(t, err)
  143. assert.Equal(t, len(links), 4)
  144. assert.Equal(t, links[0], "testProject-db-1:dbname")
  145. assert.Equal(t, links[1], "testProject-db-1:db-1")
  146. assert.Equal(t, links[2], "testProject-db-1:testProject-db-1")
  147. // ExternalLink
  148. assert.Equal(t, links[3], "db1:db2")
  149. })
  150. t.Run("service links itself oneoff", func(t *testing.T) {
  151. mockCtrl := gomock.NewController(t)
  152. defer mockCtrl.Finish()
  153. apiClient := mocks.NewMockAPIClient(mockCtrl)
  154. cli := mocks.NewMockCli(mockCtrl)
  155. tested, err := NewComposeService(cli)
  156. assert.NilError(t, err)
  157. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  158. s.Links = []string{}
  159. s.ExternalLinks = []string{}
  160. s.Labels = s.Labels.Add(api.OneoffLabel, "True")
  161. c := testContainer("web", webContainerName, true)
  162. containerListOptionsOneOff := client.ContainerListOptions{
  163. Filters: projectFilter(testProject).Add("label",
  164. serviceFilter("web"),
  165. oneOffFilter(false),
  166. hasConfigHashLabel(),
  167. ),
  168. All: true,
  169. }
  170. apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptionsOneOff).Return(client.ContainerListResult{
  171. Items: []container.Summary{c},
  172. }, nil)
  173. links, err := tested.(*composeService).getLinks(t.Context(), testProject, s, 1)
  174. assert.NilError(t, err)
  175. assert.Equal(t, len(links), 3)
  176. assert.Equal(t, links[0], "testProject-web-1:web")
  177. assert.Equal(t, links[1], "testProject-web-1:web-1")
  178. assert.Equal(t, links[2], "testProject-web-1:testProject-web-1")
  179. })
  180. }
  181. func TestWaitDependencies(t *testing.T) {
  182. mockCtrl := gomock.NewController(t)
  183. defer mockCtrl.Finish()
  184. apiClient := mocks.NewMockAPIClient(mockCtrl)
  185. cli := mocks.NewMockCli(mockCtrl)
  186. tested, err := NewComposeService(cli)
  187. assert.NilError(t, err)
  188. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  189. t.Run("should skip dependencies with scale 0", func(t *testing.T) {
  190. dbService := types.ServiceConfig{Name: "db", Scale: intPtr(0)}
  191. redisService := types.ServiceConfig{Name: "redis", Scale: intPtr(0)}
  192. project := types.Project{Name: strings.ToLower(testProject), Services: types.Services{
  193. "db": dbService,
  194. "redis": redisService,
  195. }}
  196. dependencies := types.DependsOnConfig{
  197. "db": {Condition: ServiceConditionRunningOrHealthy},
  198. "redis": {Condition: ServiceConditionRunningOrHealthy},
  199. }
  200. assert.NilError(t, tested.(*composeService).waitDependencies(t.Context(), &project, "", dependencies, nil, 0))
  201. })
  202. t.Run("should skip dependencies with condition service_started", func(t *testing.T) {
  203. dbService := types.ServiceConfig{Name: "db", Scale: intPtr(1)}
  204. redisService := types.ServiceConfig{Name: "redis", Scale: intPtr(1)}
  205. project := types.Project{Name: strings.ToLower(testProject), Services: types.Services{
  206. "db": dbService,
  207. "redis": redisService,
  208. }}
  209. dependencies := types.DependsOnConfig{
  210. "db": {Condition: types.ServiceConditionStarted, Required: true},
  211. "redis": {Condition: types.ServiceConditionStarted, Required: true},
  212. }
  213. assert.NilError(t, tested.(*composeService).waitDependencies(t.Context(), &project, "", dependencies, nil, 0))
  214. })
  215. }
  216. func TestIsServiceHealthy(t *testing.T) {
  217. mockCtrl := gomock.NewController(t)
  218. defer mockCtrl.Finish()
  219. apiClient := mocks.NewMockAPIClient(mockCtrl)
  220. cli := mocks.NewMockCli(mockCtrl)
  221. tested, err := NewComposeService(cli)
  222. assert.NilError(t, err)
  223. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  224. ctx := t.Context()
  225. t.Run("disabled healthcheck with fallback to running", func(t *testing.T) {
  226. containerID := "test-container-id"
  227. containers := Containers{
  228. {ID: containerID},
  229. }
  230. // Container with disabled healthcheck (Test: ["NONE"])
  231. apiClient.EXPECT().ContainerInspect(ctx, containerID, gomock.Any()).Return(client.ContainerInspectResult{
  232. Container: container.InspectResponse{
  233. ID: containerID,
  234. Name: "test-container",
  235. State: &container.State{Status: "running"},
  236. Config: &container.Config{
  237. Healthcheck: &container.HealthConfig{
  238. Test: []string{"NONE"},
  239. },
  240. },
  241. },
  242. }, nil)
  243. isHealthy, err := tested.(*composeService).isServiceHealthy(ctx, containers, true)
  244. assert.NilError(t, err)
  245. assert.Equal(t, true, isHealthy, "Container with disabled healthcheck should be considered healthy when running with fallbackRunning=true")
  246. })
  247. t.Run("disabled healthcheck without fallback", func(t *testing.T) {
  248. containerID := "test-container-id"
  249. containers := Containers{
  250. {ID: containerID},
  251. }
  252. // Container with disabled healthcheck (Test: ["NONE"]) but fallbackRunning=false
  253. apiClient.EXPECT().ContainerInspect(ctx, containerID, gomock.Any()).Return(client.ContainerInspectResult{
  254. Container: container.InspectResponse{
  255. ID: containerID,
  256. Name: "test-container",
  257. State: &container.State{Status: "running"},
  258. Config: &container.Config{
  259. Healthcheck: &container.HealthConfig{
  260. Test: []string{"NONE"},
  261. },
  262. },
  263. },
  264. }, nil)
  265. _, err := tested.(*composeService).isServiceHealthy(ctx, containers, false)
  266. assert.ErrorContains(t, err, "has no healthcheck configured")
  267. })
  268. t.Run("no healthcheck with fallback to running", func(t *testing.T) {
  269. containerID := "test-container-id"
  270. containers := Containers{
  271. {ID: containerID},
  272. }
  273. // Container with no healthcheck at all
  274. apiClient.EXPECT().ContainerInspect(ctx, containerID, gomock.Any()).Return(client.ContainerInspectResult{
  275. Container: container.InspectResponse{
  276. ID: containerID,
  277. Name: "test-container",
  278. State: &container.State{Status: "running"},
  279. Config: &container.Config{
  280. Healthcheck: nil,
  281. },
  282. },
  283. }, nil)
  284. isHealthy, err := tested.(*composeService).isServiceHealthy(ctx, containers, true)
  285. assert.NilError(t, err)
  286. assert.Equal(t, true, isHealthy, "Container with no healthcheck should be considered healthy when running with fallbackRunning=true")
  287. })
  288. t.Run("exited container with disabled healthcheck", func(t *testing.T) {
  289. containerID := "test-container-id"
  290. containers := Containers{
  291. {ID: containerID},
  292. }
  293. // Container with disabled healthcheck but exited
  294. apiClient.EXPECT().ContainerInspect(ctx, containerID, gomock.Any()).Return(client.ContainerInspectResult{
  295. Container: container.InspectResponse{
  296. ID: containerID,
  297. Name: "test-container",
  298. State: &container.State{
  299. Status: "exited",
  300. ExitCode: 1,
  301. },
  302. Config: &container.Config{
  303. Healthcheck: &container.HealthConfig{
  304. Test: []string{"NONE"},
  305. },
  306. },
  307. },
  308. }, nil)
  309. _, err := tested.(*composeService).isServiceHealthy(ctx, containers, true)
  310. assert.ErrorContains(t, err, "exited")
  311. })
  312. t.Run("healthy container with healthcheck", func(t *testing.T) {
  313. containerID := "test-container-id"
  314. containers := Containers{
  315. {ID: containerID},
  316. }
  317. // Container with actual healthcheck that is healthy
  318. apiClient.EXPECT().ContainerInspect(ctx, containerID, gomock.Any()).Return(client.ContainerInspectResult{
  319. Container: container.InspectResponse{
  320. ID: containerID,
  321. Name: "test-container",
  322. State: &container.State{
  323. Status: "running",
  324. Health: &container.Health{
  325. Status: container.Healthy,
  326. },
  327. },
  328. Config: &container.Config{
  329. Healthcheck: &container.HealthConfig{
  330. Test: []string{"CMD", "curl", "-f", "http://localhost"},
  331. },
  332. },
  333. },
  334. }, nil)
  335. isHealthy, err := tested.(*composeService).isServiceHealthy(ctx, containers, false)
  336. assert.NilError(t, err)
  337. assert.Equal(t, true, isHealthy, "Container with healthy status should be healthy")
  338. })
  339. }
  340. func TestCreateMobyContainer(t *testing.T) {
  341. mockCtrl := gomock.NewController(t)
  342. defer mockCtrl.Finish()
  343. apiClient := mocks.NewMockAPIClient(mockCtrl)
  344. cli := mocks.NewMockCli(mockCtrl)
  345. tested, err := NewComposeService(cli)
  346. assert.NilError(t, err)
  347. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  348. cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
  349. apiClient.EXPECT().DaemonHost().Return("").AnyTimes()
  350. apiClient.EXPECT().ImageInspect(anyCancellableContext(), gomock.Any()).Return(client.ImageInspectResult{}, nil).AnyTimes()
  351. apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}).Return(client.PingResult{
  352. APIVersion: "1.44",
  353. }, nil).AnyTimes()
  354. apiClient.EXPECT().ClientVersion().Return("1.44").AnyTimes()
  355. service := types.ServiceConfig{
  356. Name: "test",
  357. Networks: map[string]*types.ServiceNetworkConfig{
  358. "a": {
  359. Priority: 10,
  360. },
  361. "b": {
  362. Priority: 100,
  363. },
  364. },
  365. }
  366. project := types.Project{
  367. Name: "bork",
  368. Services: types.Services{
  369. "test": service,
  370. },
  371. Networks: types.Networks{
  372. "a": types.NetworkConfig{
  373. Name: "a-moby-name",
  374. },
  375. "b": types.NetworkConfig{
  376. Name: "b-moby-name",
  377. },
  378. },
  379. }
  380. var got client.ContainerCreateOptions
  381. apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, opts client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
  382. got = opts
  383. return client.ContainerCreateResult{ID: "an-id"}, nil
  384. })
  385. apiClient.EXPECT().ContainerInspect(gomock.Any(), gomock.Eq("an-id"), gomock.Any()).Times(1).Return(client.ContainerInspectResult{
  386. Container: container.InspectResponse{
  387. ID: "an-id",
  388. Name: "a-name",
  389. Config: &container.Config{},
  390. NetworkSettings: &container.NetworkSettings{},
  391. },
  392. }, nil)
  393. _, err = tested.(*composeService).createMobyContainer(t.Context(), &project, service, "test", 0, nil, createOptions{
  394. Labels: make(types.Labels),
  395. })
  396. var falseBool bool
  397. want := client.ContainerCreateOptions{
  398. Config: &container.Config{
  399. AttachStdout: true,
  400. AttachStderr: true,
  401. Image: "bork-test",
  402. Labels: map[string]string{
  403. "com.docker.compose.config-hash": "8dbce408396f8986266bc5deba0c09cfebac63c95c2238e405c7bee5f1bd84b8",
  404. "com.docker.compose.depends_on": "",
  405. },
  406. },
  407. HostConfig: &container.HostConfig{
  408. PortBindings: network.PortMap{},
  409. ExtraHosts: []string{},
  410. Tmpfs: map[string]string{},
  411. Resources: container.Resources{
  412. OomKillDisable: &falseBool,
  413. },
  414. NetworkMode: "b-moby-name",
  415. },
  416. NetworkingConfig: &network.NetworkingConfig{
  417. EndpointsConfig: map[string]*network.EndpointSettings{
  418. "a-moby-name": {
  419. IPAMConfig: &network.EndpointIPAMConfig{},
  420. Aliases: []string{"bork-test-0"},
  421. },
  422. "b-moby-name": {
  423. IPAMConfig: &network.EndpointIPAMConfig{},
  424. Aliases: []string{"bork-test-0"},
  425. },
  426. },
  427. },
  428. Name: "test",
  429. }
  430. assert.DeepEqual(t, want, got, cmpopts.EquateComparable(netip.Addr{}), cmpopts.EquateEmpty())
  431. assert.NilError(t, err)
  432. }
  433. func TestCreateMobyContainerLegacyAPI(t *testing.T) {
  434. mockCtrl := gomock.NewController(t)
  435. defer mockCtrl.Finish()
  436. apiClient := mocks.NewMockAPIClient(mockCtrl)
  437. cli := mocks.NewMockCli(mockCtrl)
  438. tested, err := NewComposeService(cli)
  439. assert.NilError(t, err)
  440. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  441. cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
  442. apiClient.EXPECT().DaemonHost().Return("").AnyTimes()
  443. apiClient.EXPECT().ImageInspect(anyCancellableContext(), gomock.Any()).
  444. Return(client.ImageInspectResult{}, nil).AnyTimes()
  445. apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}).
  446. Return(client.PingResult{APIVersion: "1.43"}, nil).AnyTimes()
  447. apiClient.EXPECT().ClientVersion().Return("1.43").AnyTimes()
  448. service := types.ServiceConfig{
  449. Name: "test",
  450. Networks: map[string]*types.ServiceNetworkConfig{
  451. "a": {Priority: 10},
  452. "b": {Priority: 100},
  453. },
  454. }
  455. project := types.Project{
  456. Name: "bork",
  457. Services: types.Services{
  458. "test": service,
  459. },
  460. Networks: types.Networks{
  461. "a": types.NetworkConfig{Name: "a-moby-name"},
  462. "b": types.NetworkConfig{Name: "b-moby-name"},
  463. },
  464. }
  465. var gotCreate client.ContainerCreateOptions
  466. apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any()).
  467. DoAndReturn(func(_ context.Context, opts client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
  468. gotCreate = opts
  469. return client.ContainerCreateResult{ID: "an-id"}, nil
  470. })
  471. // For API < 1.44, the secondary network "a" should be connected via NetworkConnect.
  472. var gotConnect client.NetworkConnectOptions
  473. connectCall := apiClient.EXPECT().
  474. NetworkConnect(gomock.Any(), gomock.Eq("a-moby-name"), gomock.Any()).
  475. DoAndReturn(func(_ context.Context, _ string, opts client.NetworkConnectOptions) (client.NetworkConnectResult, error) {
  476. gotConnect = opts
  477. return client.NetworkConnectResult{}, nil
  478. })
  479. apiClient.EXPECT().ContainerInspect(gomock.Any(), gomock.Eq("an-id"), gomock.Any()).
  480. Times(1).After(connectCall).Return(client.ContainerInspectResult{
  481. Container: container.InspectResponse{
  482. ID: "an-id",
  483. Name: "a-name",
  484. Config: &container.Config{},
  485. NetworkSettings: &container.NetworkSettings{
  486. Networks: map[string]*network.EndpointSettings{
  487. "b-moby-name": {
  488. IPAMConfig: &network.EndpointIPAMConfig{},
  489. Aliases: []string{"bork-test-0"},
  490. },
  491. "a-moby-name": {
  492. IPAMConfig: &network.EndpointIPAMConfig{},
  493. Aliases: []string{"bork-test-0"},
  494. },
  495. },
  496. },
  497. },
  498. }, nil)
  499. _, err = tested.(*composeService).createMobyContainer(t.Context(), &project, service, "test", 0, nil, createOptions{
  500. Labels: make(types.Labels),
  501. UseNetworkAliases: true,
  502. })
  503. assert.NilError(t, err)
  504. // ContainerCreate should only have the primary network (b, highest priority)
  505. assert.Check(t, gotCreate.NetworkingConfig != nil)
  506. assert.Equal(t, len(gotCreate.NetworkingConfig.EndpointsConfig), 1)
  507. _, hasPrimary := gotCreate.NetworkingConfig.EndpointsConfig["b-moby-name"]
  508. assert.Check(t, hasPrimary, "primary network b-moby-name should be in ContainerCreate EndpointsConfig")
  509. // NetworkConnect should have been called for the secondary network "a"
  510. assert.Equal(t, gotConnect.Container, "an-id")
  511. assert.Check(t, gotConnect.EndpointConfig != nil)
  512. }
  513. func TestCreateMobyContainerLegacyAPI_NetworkConnectFailure(t *testing.T) {
  514. mockCtrl := gomock.NewController(t)
  515. defer mockCtrl.Finish()
  516. apiClient := mocks.NewMockAPIClient(mockCtrl)
  517. cli := mocks.NewMockCli(mockCtrl)
  518. tested, err := NewComposeService(cli)
  519. assert.NilError(t, err)
  520. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  521. cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
  522. apiClient.EXPECT().DaemonHost().Return("").AnyTimes()
  523. apiClient.EXPECT().ImageInspect(anyCancellableContext(), gomock.Any()).
  524. Return(client.ImageInspectResult{}, nil).AnyTimes()
  525. apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}).
  526. Return(client.PingResult{APIVersion: "1.43"}, nil).AnyTimes()
  527. apiClient.EXPECT().ClientVersion().Return("1.43").AnyTimes()
  528. service := types.ServiceConfig{
  529. Name: "test",
  530. Networks: map[string]*types.ServiceNetworkConfig{
  531. "a": {Priority: 10},
  532. "b": {Priority: 100},
  533. },
  534. }
  535. project := types.Project{
  536. Name: "bork",
  537. Services: types.Services{
  538. "test": service,
  539. },
  540. Networks: types.Networks{
  541. "a": types.NetworkConfig{Name: "a-moby-name"},
  542. "b": types.NetworkConfig{Name: "b-moby-name"},
  543. },
  544. }
  545. apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any()).
  546. Return(client.ContainerCreateResult{ID: "an-id"}, nil)
  547. // NetworkConnect fails
  548. connectErr := fmt.Errorf("network connect failed")
  549. apiClient.EXPECT().NetworkConnect(gomock.Any(), gomock.Eq("a-moby-name"), gomock.Any()).
  550. Return(client.NetworkConnectResult{}, connectErr)
  551. // ContainerRemove should be called to clean up the orphan container
  552. apiClient.EXPECT().ContainerRemove(gomock.Any(), gomock.Eq("an-id"), gomock.Any()).
  553. DoAndReturn(func(_ context.Context, _ string, opts client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
  554. assert.Check(t, opts.Force, "ContainerRemove should use Force")
  555. return client.ContainerRemoveResult{}, nil
  556. })
  557. _, err = tested.(*composeService).createMobyContainer(t.Context(), &project, service, "test", 0, nil, createOptions{
  558. Labels: make(types.Labels),
  559. UseNetworkAliases: true,
  560. })
  561. assert.ErrorContains(t, err, "network connect failed")
  562. }
  563. func TestRuntimeAPIVersionCachesNegotiation(t *testing.T) {
  564. mockCtrl := gomock.NewController(t)
  565. defer mockCtrl.Finish()
  566. apiClient := mocks.NewMockAPIClient(mockCtrl)
  567. cli := mocks.NewMockCli(mockCtrl)
  568. tested := &composeService{dockerCli: cli}
  569. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  570. // Ping reports the server's max API version (1.44), but after negotiation
  571. // the client may settle on a lower version (1.43) — e.g. when the client
  572. // SDK caps at an older version. RuntimeAPIVersion must return the negotiated
  573. // ClientVersion, not the server's raw APIVersion.
  574. apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}).Return(client.PingResult{
  575. APIVersion: "1.44",
  576. }, nil).Times(1)
  577. apiClient.EXPECT().ClientVersion().Return("1.43").Times(1)
  578. version, err := tested.RuntimeAPIVersion(t.Context())
  579. assert.NilError(t, err)
  580. assert.Equal(t, version, "1.43")
  581. version, err = tested.RuntimeAPIVersion(t.Context())
  582. assert.NilError(t, err)
  583. assert.Equal(t, version, "1.43")
  584. }
  585. func TestRuntimeAPIVersionRetriesOnTransientError(t *testing.T) {
  586. mockCtrl := gomock.NewController(t)
  587. defer mockCtrl.Finish()
  588. apiClient := mocks.NewMockAPIClient(mockCtrl)
  589. cli := mocks.NewMockCli(mockCtrl)
  590. tested := &composeService{dockerCli: cli}
  591. cli.EXPECT().Client().Return(apiClient).AnyTimes()
  592. // First call: Ping fails with a transient error
  593. firstCall := apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}).
  594. Return(client.PingResult{}, context.DeadlineExceeded).Times(1)
  595. // Second call: Ping succeeds after the transient failure
  596. apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}).
  597. Return(client.PingResult{APIVersion: "1.44"}, nil).Times(1).After(firstCall)
  598. apiClient.EXPECT().ClientVersion().Return("1.44").Times(1)
  599. // First call should return the transient error
  600. _, err := tested.RuntimeAPIVersion(t.Context())
  601. assert.ErrorIs(t, err, context.DeadlineExceeded)
  602. // Second call should succeed — error was not cached
  603. version, err := tested.RuntimeAPIVersion(t.Context())
  604. assert.NilError(t, err)
  605. assert.Equal(t, version, "1.44")
  606. // Third call should return the cached value without calling Ping again
  607. version, err = tested.RuntimeAPIVersion(t.Context())
  608. assert.NilError(t, err)
  609. assert.Equal(t, version, "1.44")
  610. }