detached 84bef01

Migrate restart policy for existing managed containers

commit
84bef016501048e0e615836ffd02ead175948e60
author
brice · · 2026-05-05 09:10 UTC
parent
d3752fd
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
14051405 return health
14061406 }
14071407
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+
14081429 // SyncContainers synchronizes containers with runtime state.
14091430 func (s *Service) SyncContainers(ctx context.Context) error {
14101431 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -1413,6 +1434,8 @@ func (s *Service) SyncContainers(ctx context.Context) error {
14131434 })
14141435 log := zerowrap.FromCtx(ctx)
14151436
1437+ s.ensureManagedContainerRestartPolicies(ctx)
1438+
14161439 // List containers without holding lock to avoid blocking during Docker API call
14171440 allContainers, err := s.runtime.ListContainers(ctx, false)
14181441 if err != nil {
@@ -1405,6 +1405,27 @@ func (s *Service) HealthCheck(ctx context.Context) map[string]bool {
1405 return health1405 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 call1439 // 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)
256256 metrics, reader := setupMetricsTest(t)
257257 svc.SetMetrics(metrics)
258258
259+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once()
259260 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{
260261 {
261262 ID: "container-1",
@@ -280,6 +281,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T)
280281 assert.Equal(t, 0, points[0].Attributes.Len())
281282
282283 // No runtime change: metric should remain stable.
284+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once()
283285 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{
284286 {
285287 ID: "container-1",
@@ -304,6 +306,7 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T)
304306 assert.Equal(t, 0, points[0].Attributes.Len())
305307
306308 // One container removed in runtime: metric should decrement by one.
309+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once()
307310 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{
308311 {
309312 ID: "container-1",
@@ -1238,6 +1241,92 @@ func TestService_HealthCheck(t *testing.T) {
12381241 assert.False(t, result["unhealthy.example.com"])
12391242 }
12401243
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+
12411330 func TestService_SyncContainers(t *testing.T) {
12421331 runtime := mocks.NewMockContainerRuntime(t)
12431332 envLoader := mocks.NewMockEnvLoader(t)
@@ -1246,6 +1335,7 @@ func TestService_SyncContainers(t *testing.T) {
12461335 svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
12471336 ctx := testContext()
12481337
1338+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once()
12491339 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{
12501340 {
12511341 ID: "container-1",
@@ -1372,6 +1462,7 @@ func TestService_Restart_ReconcilesStaleContainerID(t *testing.T) {
13721462 runtime.EXPECT().RestartContainer(mock.Anything, "stale-container-id").
13731463 Return(errors.New("no container with name or ID \"stale-container-id\" found")).
13741464 Once()
1465+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once()
13751466 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{
13761467 {
13771468 ID: "fresh-container-id",
@@ -1406,6 +1497,7 @@ func TestService_Restart_ReconcilesStaleContainerID_NotFoundAfterSync(t *testing
14061497 runtime.EXPECT().RestartContainer(mock.Anything, "stale-container-id").
14071498 Return(errors.New("no such container")).
14081499 Once()
1500+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once()
14091501 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Once()
14101502
14111503 err := svc.Restart(ctx, "test.example.com", false)
@@ -1428,6 +1520,7 @@ func TestService_Restart_WithAttachments(t *testing.T) {
14281520 Status: "running",
14291521 }
14301522 svc.attachments["test.example.com"] = []string{"container-attach-1", "container-attach-2"}
1523+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe()
14311524 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{
14321525 {
14331526 ID: "container-123",
@@ -1482,6 +1575,7 @@ func TestService_Restart_WithAttachments_ErrorsWhenConfiguredButNotDeployed(t *t
14821575 Status: "running",
14831576 }
14841577 // No attachment containers tracked — s.attachments is empty
1578+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe()
14851579 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe()
14861580
14871581 err := svc.Restart(testContext(), "test.example.com", true)
@@ -1512,6 +1606,7 @@ func TestService_Restart_WithAttachments_ErrorsWhenPartiallyDeployed(t *testing.
15121606 }
15131607 // Only 1 of 2 attachments deployed
15141608 svc.attachments["test.example.com"] = []string{"attach-postgres"}
1609+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe()
15151610 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{
15161611 {
15171612 ID: "container-123",
@@ -1558,6 +1653,7 @@ func TestService_Restart_WithAttachments_SucceedsWhenAllDeployed(t *testing.T) {
15581653 Status: "running",
15591654 }
15601655 svc.attachments["test.example.com"] = []string{"attach-456"}
1656+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe()
15611657 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{
15621658 {
15631659 ID: "container-123",
@@ -1626,6 +1722,7 @@ func TestService_Restart_WithAttachments_NoConfiguredNoDeployed_Succeeds(t *test
16261722 Name: "gordon-test.example.com",
16271723 Status: "running",
16281724 }
1725+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Maybe()
16291726 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{}, nil).Maybe()
16301727
16311728 runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil)
@@ -3101,6 +3198,7 @@ func TestSyncContainersRebuildsAttachmentMap(t *testing.T) {
31013198 ctx := testContext()
31023199
31033200 // ListContainers returns one main container and one attachment container
3201+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once()
31043202 runtime.EXPECT().ListContainers(mock.Anything, false).Return([]*domain.Container{
31053203 {
31063204 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 empty1577 // 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 deployed1607 // 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 container3200 // 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",

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close