Migrate restart policy for existing managed containers
- commit
- 84bef016501048e0e615836ffd02ead175948e60
- author
- brice · · 2026-05-05 09:10 UTC
- parent
- d3752fd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +121 -0
M internal/usecase/container/service.go +23 -0
| @@ -1405,6 +1405,27 @@ func (s *Service) HealthCheck(ctx context.Context) map[string]bool { | |||
| 1405 | 1405 | return health | |
| 1406 | 1406 | } | |
| 1407 | 1407 | ||
| 1408 | + | func (s *Service) ensureManagedContainerRestartPolicies(ctx context.Context) { | |
| 1409 | + | log := zerowrap.FromCtx(ctx) | |
| 1410 | + | allContainers, err := s.runtime.ListContainers(ctx, true) | |
| 1411 | + | if err != nil { | |
| 1412 | + | log.Warn().Err(err).Msg("failed to list containers for restart policy migration") | |
| 1413 | + | return | |
| 1414 | + | } | |
| 1415 | + | ||
| 1416 | + | for _, c := range allContainers { | |
| 1417 | + | if c.Labels == nil || c.Labels[domain.LabelManaged] != "true" { | |
| 1418 | + | continue | |
| 1419 | + | } | |
| 1420 | + | if err := s.runtime.EnsureContainerRestartPolicy(ctx, c.ID, domain.RestartPolicyAlways); err != nil { | |
| 1421 | + | log.Warn().Err(err). | |
| 1422 | + | Str(zerowrap.FieldEntityID, c.ID). | |
| 1423 | + | Str("container_name", c.Name). | |
| 1424 | + | Msg("failed to ensure managed container restart policy") | |
| 1425 | + | } | |
| 1426 | + | } | |
| 1427 | + | } | |
| 1428 | + | ||
| 1408 | 1429 | // SyncContainers synchronizes containers with runtime state. | |
| 1409 | 1430 | func (s *Service) SyncContainers(ctx context.Context) error { | |
| 1410 | 1431 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| @@ -1413,6 +1434,8 @@ func (s *Service) SyncContainers(ctx context.Context) error { | |||
| 1413 | 1434 | }) | |
| 1414 | 1435 | log := zerowrap.FromCtx(ctx) | |
| 1415 | 1436 | ||
| 1437 | + | s.ensureManagedContainerRestartPolicies(ctx) | |
| 1438 | + | ||
| 1416 | 1439 | // List containers without holding lock to avoid blocking during Docker API call | |
| 1417 | 1440 | allContainers, err := s.runtime.ListContainers(ctx, false) | |
| 1418 | 1441 | if err != nil { | |
| @@ -1405,6 +1405,27 @@ func (s *Service) HealthCheck(ctx context.Context) map[string]bool { | |||||
| 1405 | return health | 1405 | return health | ||
| 1406 | } | 1406 | } | ||
| 1407 | 1407 | ||||
| 1408 | + | func (s *Service) ensureManagedContainerRestartPolicies(ctx context.Context) { | |||
| 1409 | + | log := zerowrap.FromCtx(ctx) | |||
| 1410 | + | allContainers, err := s.runtime.ListContainers(ctx, true) | |||
| 1411 | + | if err != nil { | |||
| 1412 | + | log.Warn().Err(err).Msg("failed to list containers for restart policy migration") | |||
| 1413 | + | return | |||
| 1414 | + | } | |||
| 1415 | + | ||||
| 1416 | + | for _, c := range allContainers { | |||
| 1417 | + | if c.Labels == nil || c.Labels[domain.LabelManaged] != "true" { | |||
| 1418 | + | continue | |||
| 1419 | + | } | |||
| 1420 | + | if err := s.runtime.EnsureContainerRestartPolicy(ctx, c.ID, domain.RestartPolicyAlways); err != nil { | |||
| 1421 | + | log.Warn().Err(err). | |||
| 1422 | + | Str(zerowrap.FieldEntityID, c.ID). | |||
| 1423 | + | Str("container_name", c.Name). | |||
| 1424 | + | Msg("failed to ensure managed container restart policy") | |||
| 1425 | + | } | |||
| 1426 | + | } | |||
| 1427 | + | } | |||
| 1428 | + | ||||
| 1408 | // SyncContainers synchronizes containers with runtime state. | 1429 | // SyncContainers synchronizes containers with runtime state. | ||
| 1409 | func (s *Service) SyncContainers(ctx context.Context) error { | 1430 | func (s *Service) SyncContainers(ctx context.Context) error { | ||
| 1410 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | 1431 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | ||
| @@ -1413,6 +1434,8 @@ func (s *Service) SyncContainers(ctx context.Context) error { | |||||
| 1413 | }) | 1434 | }) | ||
| 1414 | log := zerowrap.FromCtx(ctx) | 1435 | log := zerowrap.FromCtx(ctx) | ||
| 1415 | 1436 | ||||
| 1437 | + | s.ensureManagedContainerRestartPolicies(ctx) | |||
| 1438 | + | ||||
| 1416 | // List containers without holding lock to avoid blocking during Docker API call | 1439 | // List containers without holding lock to avoid blocking during Docker API call | ||
| 1417 | allContainers, err := s.runtime.ListContainers(ctx, false) | 1440 | allContainers, err := s.runtime.ListContainers(ctx, false) | ||
| 1418 | if err != nil { | 1441 | if err != nil { | ||
M internal/usecase/container/service_test.go +98 -0
| @@ -256,6 +256,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T) | |||
| 256 | 256 | metrics, reader := setupMetricsTest(t) | |
| 257 | 257 | svc.SetMetrics(metrics) | |
| 258 | 258 | ||
| 259 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |
| 259 | 260 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 260 | 261 | { | |
| 261 | 262 | ID: "container-1", | |
| @@ -280,6 +281,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T) | |||
| 280 | 281 | assert.Equal(t, 0, points[0].Attributes.Len()) | |
| 281 | 282 | ||
| 282 | 283 | // No runtime change: metric should remain stable. | |
| 284 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |
| 283 | 285 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 284 | 286 | { | |
| 285 | 287 | ID: "container-1", | |
| @@ -304,6 +306,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T) | |||
| 304 | 306 | assert.Equal(t, 0, points[0].Attributes.Len()) | |
| 305 | 307 | ||
| 306 | 308 | // One container removed in runtime: metric should decrement by one. | |
| 309 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |
| 307 | 310 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 308 | 311 | { | |
| 309 | 312 | ID: "container-1", | |
| @@ -1238,6 +1241,92 @@ func TestService_HealthCheck(t *testing.T) { | |||
| 1238 | 1241 | assert.False(t, result["unhealthy.example.com"]) | |
| 1239 | 1242 | } | |
| 1240 | 1243 | ||
| 1244 | + | func TestService_SyncContainers_EnsuresRestartPolicyForManagedRoutesAndAttachments(t *testing.T) { | |
| 1245 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 1246 | + | envLoader := mocks.NewMockEnvLoader(t) | |
| 1247 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 1248 | + | ||
| 1249 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 1250 | + | ctx := testContext() | |
| 1251 | + | ||
| 1252 | + | routeContainer := &domain.Container{ | |
| 1253 | + | ID: "route-1", | |
| 1254 | + | Name: "gordon-app.example.com", | |
| 1255 | + | Labels: map[string]string{ | |
| 1256 | + | domain.LabelManaged: "true", | |
| 1257 | + | domain.LabelDomain: "app.example.com", | |
| 1258 | + | }, | |
| 1259 | + | } | |
| 1260 | + | attachmentContainer := &domain.Container{ | |
| 1261 | + | ID: "attachment-1", | |
| 1262 | + | Name: "gordon-app-example-com-postgres", | |
| 1263 | + | Labels: map[string]string{ | |
| 1264 | + | domain.LabelManaged: "true", | |
| 1265 | + | domain.LabelAttachment: "true", | |
| 1266 | + | domain.LabelAttachedTo: "app.example.com", | |
| 1267 | + | }, | |
| 1268 | + | } | |
| 1269 | + | unmanagedContainer := &domain.Container{ | |
| 1270 | + | ID: "other-1", | |
| 1271 | + | Labels: map[string]string{}, | |
| 1272 | + | } | |
| 1273 | + | ||
| 1274 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{ | |
| 1275 | + | routeContainer, | |
| 1276 | + | attachmentContainer, | |
| 1277 | + | unmanagedContainer, | |
| 1278 | + | }, nil).Once() | |
| 1279 | + | runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "route-1", domain.RestartPolicyAlways).Return(nil).Once() | |
| 1280 | + | runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "attachment-1", domain.RestartPolicyAlways).Return(nil).Once() | |
| 1281 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 1282 | + | routeContainer, | |
| 1283 | + | attachmentContainer, | |
| 1284 | + | }, nil).Once() | |
| 1285 | + | ||
| 1286 | + | err := svc.SyncContainers(ctx) | |
| 1287 | + | ||
| 1288 | + | require.NoError(t, err) | |
| 1289 | + | tracked, exists := svc.Get(ctx, "app.example.com") | |
| 1290 | + | require.True(t, exists) | |
| 1291 | + | assert.Equal(t, "route-1", tracked.ID) | |
| 1292 | + | ||
| 1293 | + | svc.mu.RLock() | |
| 1294 | + | attachmentIDs := append([]string{}, svc.attachments["app.example.com"]...) | |
| 1295 | + | svc.mu.RUnlock() | |
| 1296 | + | assert.Equal(t, []string{"attachment-1"}, attachmentIDs) | |
| 1297 | + | assert.NotContains(t, svc.containers, "other-1") | |
| 1298 | + | runtime.AssertNumberOfCalls(t, "EnsureContainerRestartPolicy", 2) | |
| 1299 | + | } | |
| 1300 | + | ||
| 1301 | + | func TestService_SyncContainers_RestartPolicyMigrationErrorIsNonFatal(t *testing.T) { | |
| 1302 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 1303 | + | envLoader := mocks.NewMockEnvLoader(t) | |
| 1304 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 1305 | + | ||
| 1306 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 1307 | + | ctx := testContext() | |
| 1308 | + | ||
| 1309 | + | routeContainer := &domain.Container{ | |
| 1310 | + | ID: "route-1", | |
| 1311 | + | Name: "gordon-app.example.com", | |
| 1312 | + | Labels: map[string]string{ | |
| 1313 | + | domain.LabelManaged: "true", | |
| 1314 | + | domain.LabelDomain: "app.example.com", | |
| 1315 | + | }, | |
| 1316 | + | } | |
| 1317 | + | ||
| 1318 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{routeContainer}, nil).Once() | |
| 1319 | + | runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "route-1", domain.RestartPolicyAlways).Return(errors.New("update failed")).Once() | |
| 1320 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{routeContainer}, nil).Once() | |
| 1321 | + | ||
| 1322 | + | err := svc.SyncContainers(ctx) | |
| 1323 | + | ||
| 1324 | + | require.NoError(t, err) | |
| 1325 | + | tracked, exists := svc.Get(ctx, "app.example.com") | |
| 1326 | + | require.True(t, exists) | |
| 1327 | + | assert.Equal(t, "route-1", tracked.ID) | |
| 1328 | + | } | |
| 1329 | + | ||
| 1241 | 1330 | func TestService_SyncContainers(t *testing.T) { | |
| 1242 | 1331 | runtime := mocks.NewMockContainerRuntime(t) | |
| 1243 | 1332 | envLoader := mocks.NewMockEnvLoader(t) | |
| @@ -1246,6 +1335,7 @@ func TestService_SyncContainers(t *testing.T) { | |||
| 1246 | 1335 | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |
| 1247 | 1336 | ctx := testContext() | |
| 1248 | 1337 | ||
| 1338 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |
| 1249 | 1339 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 1250 | 1340 | { | |
| 1251 | 1341 | ID: "container-1", | |
| @@ -1372,6 +1462,7 @@ func TestService_Restart_ReconcilesStaleContainerID(t *testing.T) { | |||
| 1372 | 1462 | runtime.EXPECT().RestartContainer(mock.Anything, "stale-container-id"). | |
| 1373 | 1463 | Return(errors.New("no container with name or ID \"stale-container-id\" found")). | |
| 1374 | 1464 | Once() | |
| 1465 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |
| 1375 | 1466 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 1376 | 1467 | { | |
| 1377 | 1468 | ID: "fresh-container-id", | |
| @@ -1406,6 +1497,7 @@ func TestService_Restart_ReconcilesStaleContainerID_NotFoundAfterSync(t *testing | |||
| 1406 | 1497 | runtime.EXPECT().RestartContainer(mock.Anything, "stale-container-id"). | |
| 1407 | 1498 | Return(errors.New("no such container")). | |
| 1408 | 1499 | Once() | |
| 1500 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |
| 1409 | 1501 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Once() | |
| 1410 | 1502 | ||
| 1411 | 1503 | err := svc.Restart(ctx, "test.example.com", false) | |
| @@ -1428,6 +1520,7 @@ func TestService_Restart_WithAttachments(t *testing.T) { | |||
| 1428 | 1520 | Status: "running", | |
| 1429 | 1521 | } | |
| 1430 | 1522 | svc.attachments["test.example.com"] = []string{"container-attach-1", "container-attach-2"} | |
| 1523 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe() | |
| 1431 | 1524 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 1432 | 1525 | { | |
| 1433 | 1526 | ID: "container-123", | |
| @@ -1482,6 +1575,7 @@ func TestService_Restart_WithAttachments_ErrorsWhenConfiguredButNotDeployed(t *t | |||
| 1482 | 1575 | Status: "running", | |
| 1483 | 1576 | } | |
| 1484 | 1577 | // No attachment containers tracked — s.attachments is empty | |
| 1578 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe() | |
| 1485 | 1579 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe() | |
| 1486 | 1580 | ||
| 1487 | 1581 | err := svc.Restart(testContext(), "test.example.com", true) | |
| @@ -1512,6 +1606,7 @@ func TestService_Restart_WithAttachments_ErrorsWhenPartiallyDeployed(t *testing. | |||
| 1512 | 1606 | } | |
| 1513 | 1607 | // Only 1 of 2 attachments deployed | |
| 1514 | 1608 | svc.attachments["test.example.com"] = []string{"attach-postgres"} | |
| 1609 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe() | |
| 1515 | 1610 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 1516 | 1611 | { | |
| 1517 | 1612 | ID: "container-123", | |
| @@ -1558,6 +1653,7 @@ func TestService_Restart_WithAttachments_SucceedsWhenAllDeployed(t *testing.T) { | |||
| 1558 | 1653 | Status: "running", | |
| 1559 | 1654 | } | |
| 1560 | 1655 | svc.attachments["test.example.com"] = []string{"attach-456"} | |
| 1656 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe() | |
| 1561 | 1657 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 1562 | 1658 | { | |
| 1563 | 1659 | ID: "container-123", | |
| @@ -1626,6 +1722,7 @@ func TestService_Restart_WithAttachments_NoConfiguredNoDeployed_Succeeds(t *test | |||
| 1626 | 1722 | Name: "gordon-test.example.com", | |
| 1627 | 1723 | Status: "running", | |
| 1628 | 1724 | } | |
| 1725 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe() | |
| 1629 | 1726 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe() | |
| 1630 | 1727 | ||
| 1631 | 1728 | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | |
| @@ -3101,6 +3198,7 @@ func TestSyncContainersRebuildsAttachmentMap(t *testing.T) { | |||
| 3101 | 3198 | ctx := testContext() | |
| 3102 | 3199 | ||
| 3103 | 3200 | // ListContainers returns one main container and one attachment container | |
| 3201 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |
| 3104 | 3202 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |
| 3105 | 3203 | { | |
| 3106 | 3204 | ID: "main-container-1", | |
| @@ -256,6 +256,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T) | |||||
| 256 | metrics, reader := setupMetricsTest(t) | 256 | metrics, reader := setupMetricsTest(t) | ||
| 257 | svc.SetMetrics(metrics) | 257 | svc.SetMetrics(metrics) | ||
| 258 | 258 | ||||
| 259 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |||
| 259 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | 260 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | ||
| 260 | { | 261 | { | ||
| 261 | ID: "container-1", | 262 | ID: "container-1", | ||
| @@ -280,6 +281,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T) | |||||
| 280 | assert.Equal(t, 0, points[0].Attributes.Len()) | 281 | assert.Equal(t, 0, points[0].Attributes.Len()) | ||
| 281 | 282 | ||||
| 282 | // No runtime change: metric should remain stable. | 283 | // No runtime change: metric should remain stable. | ||
| 284 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |||
| 283 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | 285 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | ||
| 284 | { | 286 | { | ||
| 285 | ID: "container-1", | 287 | ID: "container-1", | ||
| @@ -304,6 +306,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T) | |||||
| 304 | assert.Equal(t, 0, points[0].Attributes.Len()) | 306 | assert.Equal(t, 0, points[0].Attributes.Len()) | ||
| 305 | 307 | ||||
| 306 | // One container removed in runtime: metric should decrement by one. | 308 | // One container removed in runtime: metric should decrement by one. | ||
| 309 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |||
| 307 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | 310 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | ||
| 308 | { | 311 | { | ||
| 309 | ID: "container-1", | 312 | ID: "container-1", | ||
| @@ -1238,6 +1241,92 @@ func TestService_HealthCheck(t *testing.T) { | |||||
| 1238 | assert.False(t, result["unhealthy.example.com"]) | 1241 | assert.False(t, result["unhealthy.example.com"]) | ||
| 1239 | } | 1242 | } | ||
| 1240 | 1243 | ||||
| 1244 | + | func TestService_SyncContainers_EnsuresRestartPolicyForManagedRoutesAndAttachments(t *testing.T) { | |||
| 1245 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 1246 | + | envLoader := mocks.NewMockEnvLoader(t) | |||
| 1247 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 1248 | + | ||||
| 1249 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |||
| 1250 | + | ctx := testContext() | |||
| 1251 | + | ||||
| 1252 | + | routeContainer := &domain.Container{ | |||
| 1253 | + | ID: "route-1", | |||
| 1254 | + | Name: "gordon-app.example.com", | |||
| 1255 | + | Labels: map[string]string{ | |||
| 1256 | + | domain.LabelManaged: "true", | |||
| 1257 | + | domain.LabelDomain: "app.example.com", | |||
| 1258 | + | }, | |||
| 1259 | + | } | |||
| 1260 | + | attachmentContainer := &domain.Container{ | |||
| 1261 | + | ID: "attachment-1", | |||
| 1262 | + | Name: "gordon-app-example-com-postgres", | |||
| 1263 | + | Labels: map[string]string{ | |||
| 1264 | + | domain.LabelManaged: "true", | |||
| 1265 | + | domain.LabelAttachment: "true", | |||
| 1266 | + | domain.LabelAttachedTo: "app.example.com", | |||
| 1267 | + | }, | |||
| 1268 | + | } | |||
| 1269 | + | unmanagedContainer := &domain.Container{ | |||
| 1270 | + | ID: "other-1", | |||
| 1271 | + | Labels: map[string]string{}, | |||
| 1272 | + | } | |||
| 1273 | + | ||||
| 1274 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{ | |||
| 1275 | + | routeContainer, | |||
| 1276 | + | attachmentContainer, | |||
| 1277 | + | unmanagedContainer, | |||
| 1278 | + | }, nil).Once() | |||
| 1279 | + | runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "route-1", domain.RestartPolicyAlways).Return(nil).Once() | |||
| 1280 | + | runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "attachment-1", domain.RestartPolicyAlways).Return(nil).Once() | |||
| 1281 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | |||
| 1282 | + | routeContainer, | |||
| 1283 | + | attachmentContainer, | |||
| 1284 | + | }, nil).Once() | |||
| 1285 | + | ||||
| 1286 | + | err := svc.SyncContainers(ctx) | |||
| 1287 | + | ||||
| 1288 | + | require.NoError(t, err) | |||
| 1289 | + | tracked, exists := svc.Get(ctx, "app.example.com") | |||
| 1290 | + | require.True(t, exists) | |||
| 1291 | + | assert.Equal(t, "route-1", tracked.ID) | |||
| 1292 | + | ||||
| 1293 | + | svc.mu.RLock() | |||
| 1294 | + | attachmentIDs := append([]string{}, svc.attachments["app.example.com"]...) | |||
| 1295 | + | svc.mu.RUnlock() | |||
| 1296 | + | assert.Equal(t, []string{"attachment-1"}, attachmentIDs) | |||
| 1297 | + | assert.NotContains(t, svc.containers, "other-1") | |||
| 1298 | + | runtime.AssertNumberOfCalls(t, "EnsureContainerRestartPolicy", 2) | |||
| 1299 | + | } | |||
| 1300 | + | ||||
| 1301 | + | func TestService_SyncContainers_RestartPolicyMigrationErrorIsNonFatal(t *testing.T) { | |||
| 1302 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 1303 | + | envLoader := mocks.NewMockEnvLoader(t) | |||
| 1304 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 1305 | + | ||||
| 1306 | + | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | |||
| 1307 | + | ctx := testContext() | |||
| 1308 | + | ||||
| 1309 | + | routeContainer := &domain.Container{ | |||
| 1310 | + | ID: "route-1", | |||
| 1311 | + | Name: "gordon-app.example.com", | |||
| 1312 | + | Labels: map[string]string{ | |||
| 1313 | + | domain.LabelManaged: "true", | |||
| 1314 | + | domain.LabelDomain: "app.example.com", | |||
| 1315 | + | }, | |||
| 1316 | + | } | |||
| 1317 | + | ||||
| 1318 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{routeContainer}, nil).Once() | |||
| 1319 | + | runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "route-1", domain.RestartPolicyAlways).Return(errors.New("update failed")).Once() | |||
| 1320 | + | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{routeContainer}, nil).Once() | |||
| 1321 | + | ||||
| 1322 | + | err := svc.SyncContainers(ctx) | |||
| 1323 | + | ||||
| 1324 | + | require.NoError(t, err) | |||
| 1325 | + | tracked, exists := svc.Get(ctx, "app.example.com") | |||
| 1326 | + | require.True(t, exists) | |||
| 1327 | + | assert.Equal(t, "route-1", tracked.ID) | |||
| 1328 | + | } | |||
| 1329 | + | ||||
| 1241 | func TestService_SyncContainers(t *testing.T) { | 1330 | func TestService_SyncContainers(t *testing.T) { | ||
| 1242 | runtime := mocks.NewMockContainerRuntime(t) | 1331 | runtime := mocks.NewMockContainerRuntime(t) | ||
| 1243 | envLoader := mocks.NewMockEnvLoader(t) | 1332 | envLoader := mocks.NewMockEnvLoader(t) | ||
| @@ -1246,6 +1335,7 @@ func TestService_SyncContainers(t *testing.T) { | |||||
| 1246 | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | 1335 | svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil) | ||
| 1247 | ctx := testContext() | 1336 | ctx := testContext() | ||
| 1248 | 1337 | ||||
| 1338 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |||
| 1249 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | 1339 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | ||
| 1250 | { | 1340 | { | ||
| 1251 | ID: "container-1", | 1341 | ID: "container-1", | ||
| @@ -1372,6 +1462,7 @@ func TestService_Restart_ReconcilesStaleContainerID(t *testing.T) { | |||||
| 1372 | runtime.EXPECT().RestartContainer(mock.Anything, "stale-container-id"). | 1462 | runtime.EXPECT().RestartContainer(mock.Anything, "stale-container-id"). | ||
| 1373 | Return(errors.New("no container with name or ID \"stale-container-id\" found")). | 1463 | Return(errors.New("no container with name or ID \"stale-container-id\" found")). | ||
| 1374 | Once() | 1464 | Once() | ||
| 1465 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |||
| 1375 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | 1466 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | ||
| 1376 | { | 1467 | { | ||
| 1377 | ID: "fresh-container-id", | 1468 | ID: "fresh-container-id", | ||
| @@ -1406,6 +1497,7 @@ func TestService_Restart_ReconcilesStaleContainerID_NotFoundAfterSync(t *testing | |||||
| 1406 | runtime.EXPECT().RestartContainer(mock.Anything, "stale-container-id"). | 1497 | runtime.EXPECT().RestartContainer(mock.Anything, "stale-container-id"). | ||
| 1407 | Return(errors.New("no such container")). | 1498 | Return(errors.New("no such container")). | ||
| 1408 | Once() | 1499 | Once() | ||
| 1500 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |||
| 1409 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Once() | 1501 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Once() | ||
| 1410 | 1502 | ||||
| 1411 | err := svc.Restart(ctx, "test.example.com", false) | 1503 | err := svc.Restart(ctx, "test.example.com", false) | ||
| @@ -1428,6 +1520,7 @@ func TestService_Restart_WithAttachments(t *testing.T) { | |||||
| 1428 | Status: "running", | 1520 | Status: "running", | ||
| 1429 | } | 1521 | } | ||
| 1430 | svc.attachments["test.example.com"] = []string{"container-attach-1", "container-attach-2"} | 1522 | svc.attachments["test.example.com"] = []string{"container-attach-1", "container-attach-2"} | ||
| 1523 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe() | |||
| 1431 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | 1524 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | ||
| 1432 | { | 1525 | { | ||
| 1433 | ID: "container-123", | 1526 | ID: "container-123", | ||
| @@ -1482,6 +1575,7 @@ func TestService_Restart_WithAttachments_ErrorsWhenConfiguredButNotDeployed(t *t | |||||
| 1482 | Status: "running", | 1575 | Status: "running", | ||
| 1483 | } | 1576 | } | ||
| 1484 | // No attachment containers tracked — s.attachments is empty | 1577 | // No attachment containers tracked — s.attachments is empty | ||
| 1578 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe() | |||
| 1485 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe() | 1579 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe() | ||
| 1486 | 1580 | ||||
| 1487 | err := svc.Restart(testContext(), "test.example.com", true) | 1581 | err := svc.Restart(testContext(), "test.example.com", true) | ||
| @@ -1512,6 +1606,7 @@ func TestService_Restart_WithAttachments_ErrorsWhenPartiallyDeployed(t *testing. | |||||
| 1512 | } | 1606 | } | ||
| 1513 | // Only 1 of 2 attachments deployed | 1607 | // Only 1 of 2 attachments deployed | ||
| 1514 | svc.attachments["test.example.com"] = []string{"attach-postgres"} | 1608 | svc.attachments["test.example.com"] = []string{"attach-postgres"} | ||
| 1609 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe() | |||
| 1515 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | 1610 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | ||
| 1516 | { | 1611 | { | ||
| 1517 | ID: "container-123", | 1612 | ID: "container-123", | ||
| @@ -1558,6 +1653,7 @@ func TestService_Restart_WithAttachments_SucceedsWhenAllDeployed(t *testing.T) { | |||||
| 1558 | Status: "running", | 1653 | Status: "running", | ||
| 1559 | } | 1654 | } | ||
| 1560 | svc.attachments["test.example.com"] = []string{"attach-456"} | 1655 | svc.attachments["test.example.com"] = []string{"attach-456"} | ||
| 1656 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe() | |||
| 1561 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | 1657 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | ||
| 1562 | { | 1658 | { | ||
| 1563 | ID: "container-123", | 1659 | ID: "container-123", | ||
| @@ -1626,6 +1722,7 @@ func TestService_Restart_WithAttachments_NoConfiguredNoDeployed_Succeeds(t *test | |||||
| 1626 | Name: "gordon-test.example.com", | 1722 | Name: "gordon-test.example.com", | ||
| 1627 | Status: "running", | 1723 | Status: "running", | ||
| 1628 | } | 1724 | } | ||
| 1725 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe() | |||
| 1629 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe() | 1726 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe() | ||
| 1630 | 1727 | ||||
| 1631 | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | 1728 | runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil) | ||
| @@ -3101,6 +3198,7 @@ func TestSyncContainersRebuildsAttachmentMap(t *testing.T) { | |||||
| 3101 | ctx := testContext() | 3198 | ctx := testContext() | ||
| 3102 | 3199 | ||||
| 3103 | // ListContainers returns one main container and one attachment container | 3200 | // ListContainers returns one main container and one attachment container | ||
| 3201 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once() | |||
| 3104 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | 3202 | runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{ | ||
| 3105 | { | 3203 | { | ||
| 3106 | ID: "main-container-1", | 3204 | ID: "main-container-1", | ||