detached 7132cc9

Set restart policy after readiness

commit
7132cc965ca94f4d02c45838a340d0e0731b34ab
author
brice · · 2026-05-05 17:51 UTC
parent
86ffd72
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +143 -73
M internal/usecase/container/service.go +82 -58
@@ -275,19 +275,18 @@ func (s *Service) buildContainerConfig(in containerConfigInput) *domain.Containe
275275 }
276276
277277 containerConfig := &domain.ContainerConfig{
278- Image: in.ImageRef,
279- Name: containerName,
280- Ports: ports,
281- Env: in.EnvVars,
282- Volumes: in.Volumes,
283- NetworkMode: in.NetworkName,
284- Hostname: in.Domain,
285- Labels: labels,
286- AutoRemove: false,
287- RestartPolicy: domain.RestartPolicyAlways,
288- MemoryLimit: cfg.DefaultMemoryLimit,
289- NanoCPUs: cfg.DefaultNanoCPUs,
290- PidsLimit: cfg.DefaultPidsLimit,
278+ Image: in.ImageRef,
279+ Name: containerName,
280+ Ports: ports,
281+ Env: in.EnvVars,
282+ Volumes: in.Volumes,
283+ NetworkMode: in.NetworkName,
284+ Hostname: in.Domain,
285+ Labels: labels,
286+ AutoRemove: false,
287+ MemoryLimit: cfg.DefaultMemoryLimit,
288+ NanoCPUs: cfg.DefaultNanoCPUs,
289+ PidsLimit: cfg.DefaultPidsLimit,
291290 }
292291 applySecurityProfile(containerConfig, cfg)
293292 return containerConfig
@@ -655,6 +654,10 @@ func (s *Service) createStartedContainer(ctx context.Context, route domain.Route
655654 return nil, deployErr
656655 }
657656 }
657+ if err := s.runtime.EnsureContainerRestartPolicy(ctx, newContainer.ID, domain.RestartPolicyAlways); err != nil {
658+ s.cleanupFailedContainer(ctx, newContainer.ID)
659+ return nil, log.WrapErr(err, "failed to set container restart policy")
660+ }
658661
659662 inspected, err := s.runtime.InspectContainer(ctx, newContainer.ID)
660663 if err != nil {
@@ -1418,13 +1421,13 @@ func (s *Service) EnsureManagedContainerRestartPolicies(ctx context.Context) err
14181421 return log.WrapErr(err, "failed to list containers for restart policy migration")
14191422 }
14201423
1421- s.ensureManagedContainerRestartPolicies(ctx, allContainers)
1422- return nil
1424+ return s.ensureManagedContainerRestartPolicies(ctx, allContainers)
14231425 }
14241426
1425-func (s *Service) ensureManagedContainerRestartPolicies(ctx context.Context, allContainers []*domain.Container) {
1427+func (s *Service) ensureManagedContainerRestartPolicies(ctx context.Context, allContainers []*domain.Container) error {
14261428 log := zerowrap.FromCtx(ctx)
14271429
1430+ var errs []error
14281431 for _, c := range allContainers {
14291432 if c.Labels == nil || c.Labels[domain.LabelManaged] != "true" {
14301433 continue
@@ -1434,8 +1437,10 @@ func (s *Service) ensureManagedContainerRestartPolicies(ctx context.Context, all
14341437 Str(zerowrap.FieldEntityID, c.ID).
14351438 Str("container_name", c.Name).
14361439 Msg("failed to ensure managed container restart policy")
1440+ errs = append(errs, fmt.Errorf("container %s: %w", c.ID, err))
14371441 }
14381442 }
1443+ return errors.Join(errs...)
14391444 }
14401445
14411446 // SyncContainers synchronizes containers with runtime state.
@@ -2616,85 +2621,104 @@ func (s *Service) deployAttachedService(ctx context.Context, ownerDomain, servic
26162621
26172622 log.Info().Str(zerowrap.FieldService, serviceImage).Msg("deploying attached service")
26182623
2619- // Ensure image (canonical ref is returned; internal pulls may use the local registry).
2620- imageRef, err := s.buildValidatedImageRef(ctx, serviceImage)
2624+ config, err := s.buildAttachmentContainerConfig(ctx, ownerDomain, serviceName, serviceImage, containerName, networkName)
26212625 if err != nil {
26222626 return err
26232627 }
2624- actualImageRef, err := s.ensureImage(ctx, imageRef)
2628+
2629+ container, err := s.runtime.CreateContainer(ctx, config)
26252630 if err != nil {
2631+ return log.WrapErr(err, "failed to create attachment container")
2632+ }
2633+
2634+ // Start container
2635+ if err := s.runtime.StartContainer(ctx, container.ID); err != nil {
2636+ s.runtime.RemoveContainer(ctx, container.ID, true)
2637+ return log.WrapErr(err, "failed to start attachment container")
2638+ }
2639+
2640+ // Wait for attachment to be ready before proceeding
2641+ if err := s.waitForAttachmentReady(ctx, container.ID, config); err != nil {
2642+ log.WrapErr(err, "attachment readiness check failed, cleaning up")
2643+ if stopErr := s.runtime.StopContainer(ctx, container.ID); stopErr != nil {
2644+ log.Warn().Err(stopErr).Str(zerowrap.FieldEntityID, container.ID).Msg("failed to stop unready attachment")
2645+ }
2646+ s.runtime.RemoveContainer(ctx, container.ID, true)
2647+ return fmt.Errorf("attachment %q not ready: %w", serviceName, err)
2648+ }
2649+ if err := s.ensureAttachmentRestartPolicy(ctx, container.ID); err != nil {
26262650 return err
26272651 }
26282652
2653+ // Track attachment
2654+ s.mu.Lock()
2655+ s.attachments[ownerDomain] = append(s.attachments[ownerDomain], container.ID)
2656+ s.mu.Unlock()
2657+
2658+ // Start log collection for attachment
2659+ s.startLogCollection(ctx, container.ID, containerName)
2660+
2661+ log.Info().Str(zerowrap.FieldEntityID, container.ID).Msg("attachment deployed successfully")
2662+ return nil
2663+}
2664+
2665+func (s *Service) buildAttachmentContainerConfig(ctx context.Context, ownerDomain, serviceName, serviceImage, containerName, networkName string) (*domain.ContainerConfig, error) {
2666+ imageRef, err := s.buildValidatedImageRef(ctx, serviceImage)
2667+ if err != nil {
2668+ return nil, err
2669+ }
2670+ actualImageRef, err := s.ensureImage(ctx, imageRef)
2671+ if err != nil {
2672+ return nil, err
2673+ }
2674+
26292675 exposedPorts := s.attachmentExposedPorts(ctx, actualImageRef)
26302676 volumes, err := s.attachmentVolumes(ctx, containerName, actualImageRef)
26312677 if err != nil {
2632- return err
2678+ return nil, err
26332679 }
26342680 envVars, err := s.attachmentEnv(ctx, containerName, actualImageRef)
26352681 if err != nil {
2636- return err
2682+ return nil, err
26372683 }
2638- envHash := hashEnvironment(envVars)
26392684
26402685 s.mu.RLock()
26412686 cfg := s.config
26422687 s.mu.RUnlock()
26432688
2644- // Create container with attachment labels (use actual ref, track original in labels)
26452689 config := &domain.ContainerConfig{
26462690 Image: actualImageRef,
26472691 Name: containerName,
2648- Hostname: serviceName, // Internal DNS: postgres, redis, etc.
2692+ Hostname: serviceName,
26492693 Aliases: []string{serviceName},
26502694 Ports: exposedPorts,
26512695 Env: envVars,
26522696 Volumes: volumes,
2653- NetworkMode: networkName, // Same network as main app
2697+ NetworkMode: networkName,
26542698 Labels: map[string]string{
26552699 domain.LabelManaged: "true",
26562700 domain.LabelAttachment: "true",
26572701 domain.LabelAttachedTo: ownerDomain,
2658- domain.LabelEnvHash: envHash,
2702+ domain.LabelEnvHash: hashEnvironment(envVars),
26592703 domain.LabelImage: serviceImage,
26602704 },
2661- MemoryLimit: cfg.DefaultMemoryLimit,
2662- NanoCPUs: cfg.DefaultNanoCPUs,
2663- PidsLimit: cfg.DefaultPidsLimit,
2664- RestartPolicy: domain.RestartPolicyAlways,
2705+ MemoryLimit: cfg.DefaultMemoryLimit,
2706+ NanoCPUs: cfg.DefaultNanoCPUs,
2707+ PidsLimit: cfg.DefaultPidsLimit,
26652708 }
26662709 applySecurityProfile(config, cfg)
2710+ return config, nil
2711+}
26672712
2668- container, err := s.runtime.CreateContainer(ctx, config)
2669- if err != nil {
2670- return log.WrapErr(err, "failed to create attachment container")
2671- }
2672-
2673- // Start container
2674- if err := s.runtime.StartContainer(ctx, container.ID); err != nil {
2675- s.runtime.RemoveContainer(ctx, container.ID, true)
2676- return log.WrapErr(err, "failed to start attachment container")
2677- }
2678-
2679- // Wait for attachment to be ready before proceeding
2680- if err := s.waitForAttachmentReady(ctx, container.ID, config); err != nil {
2681- log.WrapErr(err, "attachment readiness check failed, cleaning up")
2682- if stopErr := s.runtime.StopContainer(ctx, container.ID); stopErr != nil {
2683- log.Warn().Err(stopErr).Str(zerowrap.FieldEntityID, container.ID).Msg("failed to stop unready attachment")
2713+func (s *Service) ensureAttachmentRestartPolicy(ctx context.Context, containerID string) error {
2714+ log := zerowrap.FromCtx(ctx)
2715+ if err := s.runtime.EnsureContainerRestartPolicy(ctx, containerID, domain.RestartPolicyAlways); err != nil {
2716+ if stopErr := s.runtime.StopContainer(ctx, containerID); stopErr != nil {
2717+ log.Warn().Err(stopErr).Str(zerowrap.FieldEntityID, containerID).Msg("failed to stop attachment after restart policy update failure")
26842718 }
2685- s.runtime.RemoveContainer(ctx, container.ID, true)
2686- return fmt.Errorf("attachment %q not ready: %w", serviceName, err)
2719+ s.runtime.RemoveContainer(ctx, containerID, true)
2720+ return log.WrapErr(err, "failed to set attachment restart policy")
26872721 }
2688-
2689- // Track attachment
2690- s.mu.Lock()
2691- s.attachments[ownerDomain] = append(s.attachments[ownerDomain], container.ID)
2692- s.mu.Unlock()
2693-
2694- // Start log collection for attachment
2695- s.startLogCollection(ctx, container.ID, containerName)
2696-
2697- log.Info().Str(zerowrap.FieldEntityID, container.ID).Msg("attachment deployed successfully")
26982722 return nil
26992723 }
27002724
@@ -275,19 +275,18 @@ func (s *Service) buildContainerConfig(in containerConfigInput) *domain.Containe
275 }275 }
276 276
277 containerConfig := &domain.ContainerConfig{277 containerConfig := &domain.ContainerConfig{
278- Image: in.ImageRef,278+ Image: in.ImageRef,
279- Name: containerName,279+ Name: containerName,
280- Ports: ports,280+ Ports: ports,
281- Env: in.EnvVars,281+ Env: in.EnvVars,
282- Volumes: in.Volumes,282+ Volumes: in.Volumes,
283- NetworkMode: in.NetworkName,283+ NetworkMode: in.NetworkName,
284- Hostname: in.Domain,284+ Hostname: in.Domain,
285- Labels: labels,285+ Labels: labels,
286- AutoRemove: false,286+ AutoRemove: false,
287- RestartPolicy: domain.RestartPolicyAlways,287+ MemoryLimit: cfg.DefaultMemoryLimit,
288- MemoryLimit: cfg.DefaultMemoryLimit,288+ NanoCPUs: cfg.DefaultNanoCPUs,
289- NanoCPUs: cfg.DefaultNanoCPUs,289+ PidsLimit: cfg.DefaultPidsLimit,
290- PidsLimit: cfg.DefaultPidsLimit,
291 }290 }
292 applySecurityProfile(containerConfig, cfg)291 applySecurityProfile(containerConfig, cfg)
293 return containerConfig292 return containerConfig
@@ -655,6 +654,10 @@ func (s *Service) createStartedContainer(ctx context.Context, route domain.Route
655 return nil, deployErr654 return nil, deployErr
656 }655 }
657 }656 }
657+ if err := s.runtime.EnsureContainerRestartPolicy(ctx, newContainer.ID, domain.RestartPolicyAlways); err != nil {
658+ s.cleanupFailedContainer(ctx, newContainer.ID)
659+ return nil, log.WrapErr(err, "failed to set container restart policy")
660+ }
658 661
659 inspected, err := s.runtime.InspectContainer(ctx, newContainer.ID)662 inspected, err := s.runtime.InspectContainer(ctx, newContainer.ID)
660 if err != nil {663 if err != nil {
@@ -1418,13 +1421,13 @@ func (s *Service) EnsureManagedContainerRestartPolicies(ctx context.Context) err
1418 return log.WrapErr(err, "failed to list containers for restart policy migration")1421 return log.WrapErr(err, "failed to list containers for restart policy migration")
1419 }1422 }
1420 1423
1421- s.ensureManagedContainerRestartPolicies(ctx, allContainers)1424+ return s.ensureManagedContainerRestartPolicies(ctx, allContainers)
1422- return nil
1423 }1425 }
1424 1426
1425-func (s *Service) ensureManagedContainerRestartPolicies(ctx context.Context, allContainers []*domain.Container) {1427+func (s *Service) ensureManagedContainerRestartPolicies(ctx context.Context, allContainers []*domain.Container) error {
1426 log := zerowrap.FromCtx(ctx)1428 log := zerowrap.FromCtx(ctx)
1427 1429
1430+ var errs []error
1428 for _, c := range allContainers {1431 for _, c := range allContainers {
1429 if c.Labels == nil || c.Labels[domain.LabelManaged] != "true" {1432 if c.Labels == nil || c.Labels[domain.LabelManaged] != "true" {
1430 continue1433 continue
@@ -1434,8 +1437,10 @@ func (s *Service) ensureManagedContainerRestartPolicies(ctx context.Context, all
1434 Str(zerowrap.FieldEntityID, c.ID).1437 Str(zerowrap.FieldEntityID, c.ID).
1435 Str("container_name", c.Name).1438 Str("container_name", c.Name).
1436 Msg("failed to ensure managed container restart policy")1439 Msg("failed to ensure managed container restart policy")
1440+ errs = append(errs, fmt.Errorf("container %s: %w", c.ID, err))
1437 }1441 }
1438 }1442 }
1443+ return errors.Join(errs...)
1439 }1444 }
1440 1445
1441 // SyncContainers synchronizes containers with runtime state.1446 // SyncContainers synchronizes containers with runtime state.
@@ -2616,85 +2621,104 @@ func (s *Service) deployAttachedService(ctx context.Context, ownerDomain, servic
2616 2621
2617 log.Info().Str(zerowrap.FieldService, serviceImage).Msg("deploying attached service")2622 log.Info().Str(zerowrap.FieldService, serviceImage).Msg("deploying attached service")
2618 2623
2619- // Ensure image (canonical ref is returned; internal pulls may use the local registry).2624+ config, err := s.buildAttachmentContainerConfig(ctx, ownerDomain, serviceName, serviceImage, containerName, networkName)
2620- imageRef, err := s.buildValidatedImageRef(ctx, serviceImage)
2621 if err != nil {2625 if err != nil {
2622 return err2626 return err
2623 }2627 }
2624- actualImageRef, err := s.ensureImage(ctx, imageRef)2628+
2629+ container, err := s.runtime.CreateContainer(ctx, config)
2625 if err != nil {2630 if err != nil {
2631+ return log.WrapErr(err, "failed to create attachment container")
2632+ }
2633+
2634+ // Start container
2635+ if err := s.runtime.StartContainer(ctx, container.ID); err != nil {
2636+ s.runtime.RemoveContainer(ctx, container.ID, true)
2637+ return log.WrapErr(err, "failed to start attachment container")
2638+ }
2639+
2640+ // Wait for attachment to be ready before proceeding
2641+ if err := s.waitForAttachmentReady(ctx, container.ID, config); err != nil {
2642+ log.WrapErr(err, "attachment readiness check failed, cleaning up")
2643+ if stopErr := s.runtime.StopContainer(ctx, container.ID); stopErr != nil {
2644+ log.Warn().Err(stopErr).Str(zerowrap.FieldEntityID, container.ID).Msg("failed to stop unready attachment")
2645+ }
2646+ s.runtime.RemoveContainer(ctx, container.ID, true)
2647+ return fmt.Errorf("attachment %q not ready: %w", serviceName, err)
2648+ }
2649+ if err := s.ensureAttachmentRestartPolicy(ctx, container.ID); err != nil {
2626 return err2650 return err
2627 }2651 }
2628 2652
2653+ // Track attachment
2654+ s.mu.Lock()
2655+ s.attachments[ownerDomain] = append(s.attachments[ownerDomain], container.ID)
2656+ s.mu.Unlock()
2657+
2658+ // Start log collection for attachment
2659+ s.startLogCollection(ctx, container.ID, containerName)
2660+
2661+ log.Info().Str(zerowrap.FieldEntityID, container.ID).Msg("attachment deployed successfully")
2662+ return nil
2663+}
2664+
2665+func (s *Service) buildAttachmentContainerConfig(ctx context.Context, ownerDomain, serviceName, serviceImage, containerName, networkName string) (*domain.ContainerConfig, error) {
2666+ imageRef, err := s.buildValidatedImageRef(ctx, serviceImage)
2667+ if err != nil {
2668+ return nil, err
2669+ }
2670+ actualImageRef, err := s.ensureImage(ctx, imageRef)
2671+ if err != nil {
2672+ return nil, err
2673+ }
2674+
2629 exposedPorts := s.attachmentExposedPorts(ctx, actualImageRef)2675 exposedPorts := s.attachmentExposedPorts(ctx, actualImageRef)
2630 volumes, err := s.attachmentVolumes(ctx, containerName, actualImageRef)2676 volumes, err := s.attachmentVolumes(ctx, containerName, actualImageRef)
2631 if err != nil {2677 if err != nil {
2632- return err2678+ return nil, err
2633 }2679 }
2634 envVars, err := s.attachmentEnv(ctx, containerName, actualImageRef)2680 envVars, err := s.attachmentEnv(ctx, containerName, actualImageRef)
2635 if err != nil {2681 if err != nil {
2636- return err2682+ return nil, err
2637 }2683 }
2638- envHash := hashEnvironment(envVars)
2639 2684
2640 s.mu.RLock()2685 s.mu.RLock()
2641 cfg := s.config2686 cfg := s.config
2642 s.mu.RUnlock()2687 s.mu.RUnlock()
2643 2688
2644- // Create container with attachment labels (use actual ref, track original in labels)
2645 config := &domain.ContainerConfig{2689 config := &domain.ContainerConfig{
2646 Image: actualImageRef,2690 Image: actualImageRef,
2647 Name: containerName,2691 Name: containerName,
2648- Hostname: serviceName, // Internal DNS: postgres, redis, etc.2692+ Hostname: serviceName,
2649 Aliases: []string{serviceName},2693 Aliases: []string{serviceName},
2650 Ports: exposedPorts,2694 Ports: exposedPorts,
2651 Env: envVars,2695 Env: envVars,
2652 Volumes: volumes,2696 Volumes: volumes,
2653- NetworkMode: networkName, // Same network as main app2697+ NetworkMode: networkName,
2654 Labels: map[string]string{2698 Labels: map[string]string{
2655 domain.LabelManaged: "true",2699 domain.LabelManaged: "true",
2656 domain.LabelAttachment: "true",2700 domain.LabelAttachment: "true",
2657 domain.LabelAttachedTo: ownerDomain,2701 domain.LabelAttachedTo: ownerDomain,
2658- domain.LabelEnvHash: envHash,2702+ domain.LabelEnvHash: hashEnvironment(envVars),
2659 domain.LabelImage: serviceImage,2703 domain.LabelImage: serviceImage,
2660 },2704 },
2661- MemoryLimit: cfg.DefaultMemoryLimit,2705+ MemoryLimit: cfg.DefaultMemoryLimit,
2662- NanoCPUs: cfg.DefaultNanoCPUs,2706+ NanoCPUs: cfg.DefaultNanoCPUs,
2663- PidsLimit: cfg.DefaultPidsLimit,2707+ PidsLimit: cfg.DefaultPidsLimit,
2664- RestartPolicy: domain.RestartPolicyAlways,
2665 }2708 }
2666 applySecurityProfile(config, cfg)2709 applySecurityProfile(config, cfg)
2710+ return config, nil
2711+}
2667 2712
2668- container, err := s.runtime.CreateContainer(ctx, config)2713+func (s *Service) ensureAttachmentRestartPolicy(ctx context.Context, containerID string) error {
2669- if err != nil {2714+ log := zerowrap.FromCtx(ctx)
2670- return log.WrapErr(err, "failed to create attachment container")2715+ if err := s.runtime.EnsureContainerRestartPolicy(ctx, containerID, domain.RestartPolicyAlways); err != nil {
2671- }2716+ if stopErr := s.runtime.StopContainer(ctx, containerID); stopErr != nil {
2672-2717+ log.Warn().Err(stopErr).Str(zerowrap.FieldEntityID, containerID).Msg("failed to stop attachment after restart policy update failure")
2673- // Start container
2674- if err := s.runtime.StartContainer(ctx, container.ID); err != nil {
2675- s.runtime.RemoveContainer(ctx, container.ID, true)
2676- return log.WrapErr(err, "failed to start attachment container")
2677- }
2678-
2679- // Wait for attachment to be ready before proceeding
2680- if err := s.waitForAttachmentReady(ctx, container.ID, config); err != nil {
2681- log.WrapErr(err, "attachment readiness check failed, cleaning up")
2682- if stopErr := s.runtime.StopContainer(ctx, container.ID); stopErr != nil {
2683- log.Warn().Err(stopErr).Str(zerowrap.FieldEntityID, container.ID).Msg("failed to stop unready attachment")
2684 }2718 }
2685- s.runtime.RemoveContainer(ctx, container.ID, true)2719+ s.runtime.RemoveContainer(ctx, containerID, true)
2686- return fmt.Errorf("attachment %q not ready: %w", serviceName, err)2720+ return log.WrapErr(err, "failed to set attachment restart policy")
2687 }2721 }
2688-
2689- // Track attachment
2690- s.mu.Lock()
2691- s.attachments[ownerDomain] = append(s.attachments[ownerDomain], container.ID)
2692- s.mu.Unlock()
2693-
2694- // Start log collection for attachment
2695- s.startLogCollection(ctx, container.ID, containerName)
2696-
2697- log.Info().Str(zerowrap.FieldEntityID, container.ID).Msg("attachment deployed successfully")
2698 return nil2722 return nil
2699 }2723 }
2700 2724
M internal/usecase/container/service_test.go +61 -15
@@ -57,6 +57,11 @@ func testMinDelayConfig() Config {
5757 }
5858 }
5959
60+func allowRestartPolicyEnsure(t *testing.T, runtime *mocks.MockContainerRuntime) {
61+ t.Helper()
62+ runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
63+}
64+
6065 func setupMetricsTest(t *testing.T) (*telemetry.Metrics, *sdkmetric.ManualReader) {
6166 t.Helper()
6267
@@ -255,8 +260,6 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T)
255260 ctx := testContext()
256261 metrics, reader := setupMetricsTest(t)
257262 svc.SetMetrics(metrics)
258- runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
259-
260263 runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{
261264 {
262265 ID: "container-1",
@@ -379,6 +382,8 @@ func TestService_Deploy_Success(t *testing.T) {
379382 // Wait for ready: IsContainerRunning (first check returns true) + verify after delay
380383 runtime.EXPECT().IsContainerRunning(mock.Anything, "container-123").Return(true, nil).Times(2)
381384
385+ runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "container-123", domain.RestartPolicyAlways).Return(nil).Once()
386+
382387 // Re-inspect after start
383388 runningContainer := &domain.Container{
384389 ID: "container-123",
@@ -407,6 +412,7 @@ func TestService_Deploy_Success(t *testing.T) {
407412
408413 func TestService_Deploy_ReadinessRecoveryWindow_AllowsTransientFlap(t *testing.T) {
409414 runtime := mocks.NewMockContainerRuntime(t)
415+ allowRestartPolicyEnsure(t, runtime)
410416 envLoader := mocks.NewMockEnvLoader(t)
411417 eventBus := mocks.NewMockEventPublisher(t)
412418
@@ -473,6 +479,7 @@ func TestService_Deploy_ReadinessRecoveryWindow_AllowsTransientFlap(t *testing.T
473479
474480 func TestService_Deploy_ImagePullFailure(t *testing.T) {
475481 runtime := mocks.NewMockContainerRuntime(t)
482+ allowRestartPolicyEnsure(t, runtime)
476483 envLoader := mocks.NewMockEnvLoader(t)
477484 eventBus := mocks.NewMockEventPublisher(t)
478485
@@ -515,6 +522,7 @@ func TestIsPullFailure_ErrImagePullFailed(t *testing.T) {
515522
516523 func TestService_Deploy_DoesNotMisclassifyUnauthorizedEnvLoadFailure(t *testing.T) {
517524 runtime := mocks.NewMockContainerRuntime(t)
525+ allowRestartPolicyEnsure(t, runtime)
518526 envLoader := mocks.NewMockEnvLoader(t)
519527 eventBus := mocks.NewMockEventPublisher(t)
520528
@@ -555,6 +563,7 @@ func TestService_CaptureRecentContainerLogs_SkipsCanceledContext(t *testing.T) {
555563
556564 func TestService_Deploy_ReadinessFailureReturnsDeployFailureWithLogs(t *testing.T) {
557565 runtime := mocks.NewMockContainerRuntime(t)
566+ allowRestartPolicyEnsure(t, runtime)
558567 envLoader := mocks.NewMockEnvLoader(t)
559568 eventBus := mocks.NewMockEventPublisher(t)
560569
@@ -614,6 +623,7 @@ func TestService_Deploy_ReadinessFailureReturnsDeployFailureWithLogs(t *testing.
614623
615624 func TestService_Deploy_StrictHealthModeWithoutHealthcheckReturnsHint(t *testing.T) {
616625 runtime := mocks.NewMockContainerRuntime(t)
626+ allowRestartPolicyEnsure(t, runtime)
617627 envLoader := mocks.NewMockEnvLoader(t)
618628 eventBus := mocks.NewMockEventPublisher(t)
619629
@@ -725,6 +735,7 @@ func TestService_PullImage_InternalRetriesOnConnectionRefused(t *testing.T) {
725735
726736 func TestService_Deploy_ReplacesExistingContainer(t *testing.T) {
727737 runtime := mocks.NewMockContainerRuntime(t)
738+ allowRestartPolicyEnsure(t, runtime)
728739 envLoader := mocks.NewMockEnvLoader(t)
729740 eventBus := mocks.NewMockEventPublisher(t)
730741 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -808,6 +819,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) {
808819 // from killing the real active container after a Gordon restart.
809820 func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T) {
810821 runtime := mocks.NewMockContainerRuntime(t)
822+ allowRestartPolicyEnsure(t, runtime)
811823 envLoader := mocks.NewMockEnvLoader(t)
812824 eventBus := mocks.NewMockEventPublisher(t)
813825 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -907,6 +919,7 @@ func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T
907919
908920 func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) {
909921 runtime := mocks.NewMockContainerRuntime(t)
922+ allowRestartPolicyEnsure(t, runtime)
910923 envLoader := mocks.NewMockEnvLoader(t)
911924 eventBus := mocks.NewMockEventPublisher(t)
912925
@@ -969,6 +982,7 @@ func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) {
969982
970983 func TestService_Deploy_WithNetworkIsolation(t *testing.T) {
971984 runtime := mocks.NewMockContainerRuntime(t)
985+ allowRestartPolicyEnsure(t, runtime)
972986 envLoader := mocks.NewMockEnvLoader(t)
973987 eventBus := mocks.NewMockEventPublisher(t)
974988
@@ -1027,6 +1041,7 @@ func TestService_Deploy_WithNetworkIsolation(t *testing.T) {
10271041
10281042 func TestService_Deploy_WithVolumeAutoCreate(t *testing.T) {
10291043 runtime := mocks.NewMockContainerRuntime(t)
1044+ allowRestartPolicyEnsure(t, runtime)
10301045 envLoader := mocks.NewMockEnvLoader(t)
10311046 eventBus := mocks.NewMockEventPublisher(t)
10321047
@@ -1295,7 +1310,7 @@ func TestService_EnsureManagedContainerRestartPolicies_EnsuresRoutesAttachmentsA
12951310 runtime.AssertNumberOfCalls(t, "EnsureContainerRestartPolicy", 3)
12961311 }
12971312
1298-func TestService_EnsureManagedContainerRestartPolicies_ErrorIsNonFatal(t *testing.T) {
1313+func TestService_EnsureManagedContainerRestartPolicies_ContinuesAndReturnsError(t *testing.T) {
12991314 runtime := mocks.NewMockContainerRuntime(t)
13001315 envLoader := mocks.NewMockEnvLoader(t)
13011316 eventBus := mocks.NewMockEventPublisher(t)
@@ -1311,12 +1326,23 @@ func TestService_EnsureManagedContainerRestartPolicies_ErrorIsNonFatal(t *testin
13111326 domain.LabelDomain: "app.example.com",
13121327 },
13131328 }
1329+ routeContainer2 := &domain.Container{
1330+ ID: "route-2",
1331+ Name: "gordon-api.example.com",
1332+ Labels: map[string]string{
1333+ domain.LabelManaged: "true",
1334+ domain.LabelDomain: "api.example.com",
1335+ },
1336+ }
13141337
1315- runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{routeContainer}, nil).Once()
1338+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{routeContainer, routeContainer2}, nil).Once()
13161339 runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "route-1", domain.RestartPolicyAlways).Return(errors.New("update failed")).Once()
1340+ runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "route-2", domain.RestartPolicyAlways).Return(nil).Once()
13171341 err := svc.EnsureManagedContainerRestartPolicies(ctx)
13181342
1319- require.NoError(t, err)
1343+ require.Error(t, err)
1344+ assert.Contains(t, err.Error(), "route-1")
1345+ assert.Contains(t, err.Error(), "update failed")
13201346 }
13211347
13221348 func TestService_SyncContainers(t *testing.T) {
@@ -1327,7 +1353,6 @@ func TestService_SyncContainers(t *testing.T) {
13271353 svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
13281354 ctx := testContext()
13291355
1330- runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
13311356 runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{
13321357 {
13331358 ID: "container-1",
@@ -1535,8 +1560,6 @@ func TestService_Restart_WithAttachments(t *testing.T) {
15351560 },
15361561 },
15371562 }, nil).Maybe()
1538- runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
1539-
15401563 runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil)
15411564 runtime.EXPECT().RestartContainer(mock.Anything, "container-attach-1").Return(nil)
15421565 runtime.EXPECT().RestartContainer(mock.Anything, "container-attach-2").Return(fmt.Errorf("boom"))
@@ -1612,8 +1635,6 @@ func TestService_Restart_WithAttachments_ErrorsWhenPartiallyDeployed(t *testing.
16121635 },
16131636 },
16141637 }, nil).Maybe()
1615- runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
1616-
16171638 err := svc.Restart(testContext(), "test.example.com", true)
16181639
16191640 assert.Error(t, err)
@@ -1659,8 +1680,6 @@ func TestService_Restart_WithAttachments_SucceedsWhenAllDeployed(t *testing.T) {
16591680 },
16601681 },
16611682 }, nil).Maybe()
1662- runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
1663-
16641683 runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil)
16651684 runtime.EXPECT().RestartContainer(mock.Anything, "attach-456").Return(nil)
16661685
@@ -2036,6 +2055,7 @@ func TestRewriteToLocalRegistry(t *testing.T) {
20362055
20372056 func TestService_Deploy_InternalDeployForcesPull(t *testing.T) {
20382057 runtime := mocks.NewMockContainerRuntime(t)
2058+ allowRestartPolicyEnsure(t, runtime)
20392059 envLoader := mocks.NewMockEnvLoader(t)
20402060 eventBus := mocks.NewMockEventPublisher(t)
20412061
@@ -2116,6 +2136,7 @@ func TestService_Deploy_InternalDeployForcesPull(t *testing.T) {
21162136
21172137 func TestService_AutoStart_StartsNewContainers(t *testing.T) {
21182138 runtime := mocks.NewMockContainerRuntime(t)
2139+ allowRestartPolicyEnsure(t, runtime)
21192140 envLoader := mocks.NewMockEnvLoader(t)
21202141 eventBus := mocks.NewMockEventPublisher(t)
21212142
@@ -2165,6 +2186,7 @@ func TestService_AutoStart_StartsNewContainers(t *testing.T) {
21652186
21662187 func TestService_AutoStart_SkipsExistingContainers(t *testing.T) {
21672188 runtime := mocks.NewMockContainerRuntime(t)
2189+ allowRestartPolicyEnsure(t, runtime)
21682190 envLoader := mocks.NewMockEnvLoader(t)
21692191 eventBus := mocks.NewMockEventPublisher(t)
21702192
@@ -2214,6 +2236,7 @@ func TestService_AutoStart_SkipsExistingContainers(t *testing.T) {
22142236
22152237 func TestService_AutoStart_HandlesDeployErrors(t *testing.T) {
22162238 runtime := mocks.NewMockContainerRuntime(t)
2239+ allowRestartPolicyEnsure(t, runtime)
22172240 envLoader := mocks.NewMockEnvLoader(t)
22182241 eventBus := mocks.NewMockEventPublisher(t)
22192242
@@ -2242,6 +2265,7 @@ func TestService_AutoStart_HandlesDeployErrors(t *testing.T) {
22422265
22432266 func TestService_AutoStart_UsesInternalDeployContext(t *testing.T) {
22442267 runtime := mocks.NewMockContainerRuntime(t)
2268+ allowRestartPolicyEnsure(t, runtime)
22452269 envLoader := mocks.NewMockEnvLoader(t)
22462270 eventBus := mocks.NewMockEventPublisher(t)
22472271
@@ -2294,6 +2318,7 @@ func TestService_AutoStart_UsesInternalDeployContext(t *testing.T) {
22942318 // until the new container is ready and traffic is switched.
22952319 func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) {
22962320 runtime := mocks.NewMockContainerRuntime(t)
2321+ allowRestartPolicyEnsure(t, runtime)
22972322 envLoader := mocks.NewMockEnvLoader(t)
22982323 eventBus := mocks.NewMockEventPublisher(t)
22992324 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -2390,6 +2415,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) {
23902415 // Running canonical containers are preserved (see NeverKillsRunningCanonical test).
23912416 func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) {
23922417 runtime := mocks.NewMockContainerRuntime(t)
2418+ allowRestartPolicyEnsure(t, runtime)
23932419 envLoader := mocks.NewMockEnvLoader(t)
23942420 eventBus := mocks.NewMockEventPublisher(t)
23952421
@@ -2470,6 +2496,7 @@ func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) {
24702496
24712497 func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) {
24722498 runtime := mocks.NewMockContainerRuntime(t)
2499+ allowRestartPolicyEnsure(t, runtime)
24732500 envLoader := mocks.NewMockEnvLoader(t)
24742501 eventBus := mocks.NewMockEventPublisher(t)
24752502 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -2542,6 +2569,7 @@ func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) {
25422569
25432570 func TestService_Deploy_TrackedTempContainerUsesAlternateTempName(t *testing.T) {
25442571 runtime := mocks.NewMockContainerRuntime(t)
2572+ allowRestartPolicyEnsure(t, runtime)
25452573 envLoader := mocks.NewMockEnvLoader(t)
25462574 eventBus := mocks.NewMockEventPublisher(t)
25472575 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -2731,6 +2759,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) {
27312759 // Verify that concurrent Deploy calls for the same domain are serialized
27322760 // and both succeed without container name conflicts.
27332761 runtime := mocks.NewMockContainerRuntime(t)
2762+ allowRestartPolicyEnsure(t, runtime)
27342763 envLoader := mocks.NewMockEnvLoader(t)
27352764 eventBus := mocks.NewMockEventPublisher(t)
27362765 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -2841,6 +2870,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) {
28412870 func TestService_Deploy_ContextCancellation(t *testing.T) {
28422871 // Verify that deploy lock acquisition respects context cancellation.
28432872 runtime := mocks.NewMockContainerRuntime(t)
2873+ allowRestartPolicyEnsure(t, runtime)
28442874 envLoader := mocks.NewMockEnvLoader(t)
28452875 eventBus := mocks.NewMockEventPublisher(t)
28462876
@@ -2871,6 +2901,7 @@ func TestService_Deploy_ContextCancellation(t *testing.T) {
28712901 // BEFORE the old container is stopped, preventing 503 errors during deployment.
28722902 func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) {
28732903 runtime := mocks.NewMockContainerRuntime(t)
2904+ allowRestartPolicyEnsure(t, runtime)
28742905 envLoader := mocks.NewMockEnvLoader(t)
28752906 eventBus := mocks.NewMockEventPublisher(t)
28762907 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -2951,6 +2982,7 @@ func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) {
29512982 // when no cache invalidator is set (e.g., in tests or minimal configurations).
29522983 func TestService_Deploy_NilCacheInvalidator(t *testing.T) {
29532984 runtime := mocks.NewMockContainerRuntime(t)
2985+ allowRestartPolicyEnsure(t, runtime)
29542986 envLoader := mocks.NewMockEnvLoader(t)
29552987 eventBus := mocks.NewMockEventPublisher(t)
29562988
@@ -3000,6 +3032,7 @@ func TestService_Deploy_NilCacheInvalidator(t *testing.T) {
30003032
30013033 func TestService_Deploy_SkipsRedundantDeploy(t *testing.T) {
30023034 runtime := mocks.NewMockContainerRuntime(t)
3035+ allowRestartPolicyEnsure(t, runtime)
30033036 envLoader := mocks.NewMockEnvLoader(t)
30043037 eventBus := mocks.NewMockEventPublisher(t)
30053038
@@ -3060,6 +3093,7 @@ func TestService_Deploy_SkipsRedundantDeploy(t *testing.T) {
30603093
30613094 func TestService_Deploy_SkipsRedundantDeploy_WhenTrackedImageIDMissing(t *testing.T) {
30623095 runtime := mocks.NewMockContainerRuntime(t)
3096+ allowRestartPolicyEnsure(t, runtime)
30633097 envLoader := mocks.NewMockEnvLoader(t)
30643098 eventBus := mocks.NewMockEventPublisher(t)
30653099
@@ -3121,6 +3155,7 @@ func TestService_Deploy_SkipsRedundantDeploy_WhenTrackedImageIDMissing(t *testin
31213155
31223156 func TestService_Deploy_DoesNotSkipWhenImageIDDiffers(t *testing.T) {
31233157 runtime := mocks.NewMockContainerRuntime(t)
3158+ allowRestartPolicyEnsure(t, runtime)
31243159 envLoader := mocks.NewMockEnvLoader(t)
31253160 eventBus := mocks.NewMockEventPublisher(t)
31263161 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -3223,6 +3258,7 @@ func TestSyncContainersRebuildsAttachmentMap(t *testing.T) {
32233258
32243259 func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) {
32253260 runtime := mocks.NewMockContainerRuntime(t)
3261+ allowRestartPolicyEnsure(t, runtime)
32263262 envLoader := mocks.NewMockEnvLoader(t)
32273263 eventBus := mocks.NewMockEventPublisher(t)
32283264
@@ -3286,6 +3322,7 @@ func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) {
32863322
32873323 func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) {
32883324 runtime := mocks.NewMockContainerRuntime(t)
3325+ allowRestartPolicyEnsure(t, runtime)
32893326 envLoader := mocks.NewMockEnvLoader(t)
32903327 eventBus := mocks.NewMockEventPublisher(t)
32913328
@@ -3344,6 +3381,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) {
33443381
33453382 func TestService_Deploy_DoesNotSkip_WhenEnvHashMissing(t *testing.T) {
33463383 runtime := mocks.NewMockContainerRuntime(t)
3384+ allowRestartPolicyEnsure(t, runtime)
33473385 envLoader := mocks.NewMockEnvLoader(t)
33483386 eventBus := mocks.NewMockEventPublisher(t)
33493387
@@ -3470,6 +3508,7 @@ func TestService_attachmentEnvDrifted_ReturnsTrueWhenHashMissing(t *testing.T) {
34703508 // disallows creating a container with the same name as an existing running container.
34713509 func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) {
34723510 runtime := mocks.NewMockContainerRuntime(t)
3511+ allowRestartPolicyEnsure(t, runtime)
34733512 envLoader := mocks.NewMockEnvLoader(t)
34743513 eventBus := mocks.NewMockEventPublisher(t)
34753514
@@ -3566,6 +3605,7 @@ func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) {
35663605 // and the failed new container is cleaned up.
35673606 func TestService_Deploy_RollbackOnPostSwitchCrash(t *testing.T) {
35683607 runtime := mocks.NewMockContainerRuntime(t)
3608+ allowRestartPolicyEnsure(t, runtime)
35693609 envLoader := mocks.NewMockEnvLoader(t)
35703610 eventBus := mocks.NewMockEventPublisher(t)
35713611 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -3660,6 +3700,7 @@ func TestService_Deploy_RollbackOnPostSwitchCrash(t *testing.T) {
36603700 // container is finalized (stopped and removed).
36613701 func TestService_Deploy_StabilizationSuccess(t *testing.T) {
36623702 runtime := mocks.NewMockContainerRuntime(t)
3703+ allowRestartPolicyEnsure(t, runtime)
36633704 envLoader := mocks.NewMockEnvLoader(t)
36643705 eventBus := mocks.NewMockEventPublisher(t)
36653706 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -3748,6 +3789,7 @@ func TestService_Deploy_StabilizationSuccess(t *testing.T) {
37483789 // Uses testify's NotBefore() to enforce mock call ordering.
37493790 func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T) {
37503791 runtime := mocks.NewMockContainerRuntime(t)
3792+ allowRestartPolicyEnsure(t, runtime)
37513793 envLoader := mocks.NewMockEnvLoader(t)
37523794 eventBus := mocks.NewMockEventPublisher(t)
37533795 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -3844,6 +3886,7 @@ func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T)
38443886 // the old container remains as the tracked container.
38453887 func TestService_Deploy_ZeroDowntime_ReadinessFailure_OldUntouched(t *testing.T) {
38463888 runtime := mocks.NewMockContainerRuntime(t)
3889+ allowRestartPolicyEnsure(t, runtime)
38473890 envLoader := mocks.NewMockEnvLoader(t)
38483891 eventBus := mocks.NewMockEventPublisher(t)
38493892
@@ -3934,6 +3977,7 @@ func TestService_Deploy_ZeroDowntime_ReadinessFailure_OldUntouched(t *testing.T)
39343977 // finalize/drain logic being triggered.
39353978 func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) {
39363979 runtime := mocks.NewMockContainerRuntime(t)
3980+ allowRestartPolicyEnsure(t, runtime)
39373981 envLoader := mocks.NewMockEnvLoader(t)
39383982 eventBus := mocks.NewMockEventPublisher(t)
39393983
@@ -4010,6 +4054,7 @@ func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) {
40104054
40114055 func TestService_Deploy_PropagatesImageLabelsToContainerConfig(t *testing.T) {
40124056 runtime := mocks.NewMockContainerRuntime(t)
4057+ allowRestartPolicyEnsure(t, runtime)
40134058 envLoader := mocks.NewMockEnvLoader(t)
40144059 eventBus := mocks.NewMockEventPublisher(t)
40154060
@@ -4267,7 +4312,7 @@ func TestDeployAttachedService_SetsAliasOnContainerConfig(t *testing.T) {
42674312 assert.Equal(t, []string{"postgres"}, cfg.Aliases, "attachment container must have network aliases for DNS resolution")
42684313 assert.Equal(t, "postgres", cfg.Hostname)
42694314 assert.Equal(t, networkName, cfg.NetworkMode)
4270- assert.Equal(t, domain.RestartPolicyAlways, cfg.RestartPolicy)
4315+ assert.Empty(t, cfg.RestartPolicy)
42714316 }).
42724317 Return(&domain.Container{ID: "pg-container-1", Name: containerName}, nil)
42734318
@@ -4280,6 +4325,7 @@ func TestDeployAttachedService_SetsAliasOnContainerConfig(t *testing.T) {
42804325 runtime.EXPECT().GetContainerHealthStatus(mock.Anything, "pg-container-1").Return("", false, nil)
42814326 // TCP probe: resolve endpoint fails -> falls back to delay
42824327 runtime.EXPECT().GetContainerNetworkInfo(mock.Anything, "pg-container-1").Return("", 0, errors.New("no network"))
4328+ runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "pg-container-1", domain.RestartPolicyAlways).Return(nil).Once()
42834329
42844330 err := svc.deployAttachedService(ctx, ownerDomain, serviceImage, networkName)
42854331 require.NoError(t, err)
@@ -4405,10 +4451,10 @@ func TestService_BuildContainerConfig_CompatSecurityProfileDefault(t *testing.T)
44054451 assert.Nil(t, cfg.CapAdd)
44064452 }
44074453
4408-func TestService_BuildContainerConfig_SetsRestartPolicyAlways(t *testing.T) {
4454+func TestService_BuildContainerConfig_OmitsRestartPolicyBeforeReadiness(t *testing.T) {
44094455 svc := NewService(nil, nil, nil, nil, Config{}, nil)
44104456
44114457 cfg := svc.buildContainerConfig(containerConfigInput{Domain: "app.example.com", Image: "app:latest", ImageRef: "app:latest"})
44124458
4413- assert.Equal(t, domain.RestartPolicyAlways, cfg.RestartPolicy)
4459+ assert.Empty(t, cfg.RestartPolicy)
44144460 }
@@ -57,6 +57,11 @@ func testMinDelayConfig() Config {
57 }57 }
58 }58 }
59 59
60+func allowRestartPolicyEnsure(t *testing.T, runtime *mocks.MockContainerRuntime) {
61+ t.Helper()
62+ runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
63+}
64+
60 func setupMetricsTest(t *testing.T) (*telemetry.Metrics, *sdkmetric.ManualReader) {65 func setupMetricsTest(t *testing.T) (*telemetry.Metrics, *sdkmetric.ManualReader) {
61 t.Helper()66 t.Helper()
62 67
@@ -255,8 +260,6 @@ func TestService_SyncContainers_ManagedContainersMetricTracksDelta(t *testing.T)
255 ctx := testContext()260 ctx := testContext()
256 metrics, reader := setupMetricsTest(t)261 metrics, reader := setupMetricsTest(t)
257 svc.SetMetrics(metrics)262 svc.SetMetrics(metrics)
258- runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
259-
260 runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{263 runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{
261 {264 {
262 ID: "container-1",265 ID: "container-1",
@@ -379,6 +382,8 @@ func TestService_Deploy_Success(t *testing.T) {
379 // Wait for ready: IsContainerRunning (first check returns true) + verify after delay382 // Wait for ready: IsContainerRunning (first check returns true) + verify after delay
380 runtime.EXPECT().IsContainerRunning(mock.Anything, "container-123").Return(true, nil).Times(2)383 runtime.EXPECT().IsContainerRunning(mock.Anything, "container-123").Return(true, nil).Times(2)
381 384
385+ runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "container-123", domain.RestartPolicyAlways).Return(nil).Once()
386+
382 // Re-inspect after start387 // Re-inspect after start
383 runningContainer := &domain.Container{388 runningContainer := &domain.Container{
384 ID: "container-123",389 ID: "container-123",
@@ -407,6 +412,7 @@ func TestService_Deploy_Success(t *testing.T) {
407 412
408 func TestService_Deploy_ReadinessRecoveryWindow_AllowsTransientFlap(t *testing.T) {413 func TestService_Deploy_ReadinessRecoveryWindow_AllowsTransientFlap(t *testing.T) {
409 runtime := mocks.NewMockContainerRuntime(t)414 runtime := mocks.NewMockContainerRuntime(t)
415+ allowRestartPolicyEnsure(t, runtime)
410 envLoader := mocks.NewMockEnvLoader(t)416 envLoader := mocks.NewMockEnvLoader(t)
411 eventBus := mocks.NewMockEventPublisher(t)417 eventBus := mocks.NewMockEventPublisher(t)
412 418
@@ -473,6 +479,7 @@ func TestService_Deploy_ReadinessRecoveryWindow_AllowsTransientFlap(t *testing.T
473 479
474 func TestService_Deploy_ImagePullFailure(t *testing.T) {480 func TestService_Deploy_ImagePullFailure(t *testing.T) {
475 runtime := mocks.NewMockContainerRuntime(t)481 runtime := mocks.NewMockContainerRuntime(t)
482+ allowRestartPolicyEnsure(t, runtime)
476 envLoader := mocks.NewMockEnvLoader(t)483 envLoader := mocks.NewMockEnvLoader(t)
477 eventBus := mocks.NewMockEventPublisher(t)484 eventBus := mocks.NewMockEventPublisher(t)
478 485
@@ -515,6 +522,7 @@ func TestIsPullFailure_ErrImagePullFailed(t *testing.T) {
515 522
516 func TestService_Deploy_DoesNotMisclassifyUnauthorizedEnvLoadFailure(t *testing.T) {523 func TestService_Deploy_DoesNotMisclassifyUnauthorizedEnvLoadFailure(t *testing.T) {
517 runtime := mocks.NewMockContainerRuntime(t)524 runtime := mocks.NewMockContainerRuntime(t)
525+ allowRestartPolicyEnsure(t, runtime)
518 envLoader := mocks.NewMockEnvLoader(t)526 envLoader := mocks.NewMockEnvLoader(t)
519 eventBus := mocks.NewMockEventPublisher(t)527 eventBus := mocks.NewMockEventPublisher(t)
520 528
@@ -555,6 +563,7 @@ func TestService_CaptureRecentContainerLogs_SkipsCanceledContext(t *testing.T) {
555 563
556 func TestService_Deploy_ReadinessFailureReturnsDeployFailureWithLogs(t *testing.T) {564 func TestService_Deploy_ReadinessFailureReturnsDeployFailureWithLogs(t *testing.T) {
557 runtime := mocks.NewMockContainerRuntime(t)565 runtime := mocks.NewMockContainerRuntime(t)
566+ allowRestartPolicyEnsure(t, runtime)
558 envLoader := mocks.NewMockEnvLoader(t)567 envLoader := mocks.NewMockEnvLoader(t)
559 eventBus := mocks.NewMockEventPublisher(t)568 eventBus := mocks.NewMockEventPublisher(t)
560 569
@@ -614,6 +623,7 @@ func TestService_Deploy_ReadinessFailureReturnsDeployFailureWithLogs(t *testing.
614 623
615 func TestService_Deploy_StrictHealthModeWithoutHealthcheckReturnsHint(t *testing.T) {624 func TestService_Deploy_StrictHealthModeWithoutHealthcheckReturnsHint(t *testing.T) {
616 runtime := mocks.NewMockContainerRuntime(t)625 runtime := mocks.NewMockContainerRuntime(t)
626+ allowRestartPolicyEnsure(t, runtime)
617 envLoader := mocks.NewMockEnvLoader(t)627 envLoader := mocks.NewMockEnvLoader(t)
618 eventBus := mocks.NewMockEventPublisher(t)628 eventBus := mocks.NewMockEventPublisher(t)
619 629
@@ -725,6 +735,7 @@ func TestService_PullImage_InternalRetriesOnConnectionRefused(t *testing.T) {
725 735
726 func TestService_Deploy_ReplacesExistingContainer(t *testing.T) {736 func TestService_Deploy_ReplacesExistingContainer(t *testing.T) {
727 runtime := mocks.NewMockContainerRuntime(t)737 runtime := mocks.NewMockContainerRuntime(t)
738+ allowRestartPolicyEnsure(t, runtime)
728 envLoader := mocks.NewMockEnvLoader(t)739 envLoader := mocks.NewMockEnvLoader(t)
729 eventBus := mocks.NewMockEventPublisher(t)740 eventBus := mocks.NewMockEventPublisher(t)
730 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)741 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -808,6 +819,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) {
808 // from killing the real active container after a Gordon restart.819 // from killing the real active container after a Gordon restart.
809 func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T) {820 func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T) {
810 runtime := mocks.NewMockContainerRuntime(t)821 runtime := mocks.NewMockContainerRuntime(t)
822+ allowRestartPolicyEnsure(t, runtime)
811 envLoader := mocks.NewMockEnvLoader(t)823 envLoader := mocks.NewMockEnvLoader(t)
812 eventBus := mocks.NewMockEventPublisher(t)824 eventBus := mocks.NewMockEventPublisher(t)
813 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)825 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -907,6 +919,7 @@ func TestService_Deploy_ResolvesExistingFromRuntime_WhenMemoryStale(t *testing.T
907 919
908 func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) {920 func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) {
909 runtime := mocks.NewMockContainerRuntime(t)921 runtime := mocks.NewMockContainerRuntime(t)
922+ allowRestartPolicyEnsure(t, runtime)
910 envLoader := mocks.NewMockEnvLoader(t)923 envLoader := mocks.NewMockEnvLoader(t)
911 eventBus := mocks.NewMockEventPublisher(t)924 eventBus := mocks.NewMockEventPublisher(t)
912 925
@@ -969,6 +982,7 @@ func TestService_Deploy_SkipRedundantDeploy_GetImageIDError(t *testing.T) {
969 982
970 func TestService_Deploy_WithNetworkIsolation(t *testing.T) {983 func TestService_Deploy_WithNetworkIsolation(t *testing.T) {
971 runtime := mocks.NewMockContainerRuntime(t)984 runtime := mocks.NewMockContainerRuntime(t)
985+ allowRestartPolicyEnsure(t, runtime)
972 envLoader := mocks.NewMockEnvLoader(t)986 envLoader := mocks.NewMockEnvLoader(t)
973 eventBus := mocks.NewMockEventPublisher(t)987 eventBus := mocks.NewMockEventPublisher(t)
974 988
@@ -1027,6 +1041,7 @@ func TestService_Deploy_WithNetworkIsolation(t *testing.T) {
1027 1041
1028 func TestService_Deploy_WithVolumeAutoCreate(t *testing.T) {1042 func TestService_Deploy_WithVolumeAutoCreate(t *testing.T) {
1029 runtime := mocks.NewMockContainerRuntime(t)1043 runtime := mocks.NewMockContainerRuntime(t)
1044+ allowRestartPolicyEnsure(t, runtime)
1030 envLoader := mocks.NewMockEnvLoader(t)1045 envLoader := mocks.NewMockEnvLoader(t)
1031 eventBus := mocks.NewMockEventPublisher(t)1046 eventBus := mocks.NewMockEventPublisher(t)
1032 1047
@@ -1295,7 +1310,7 @@ func TestService_EnsureManagedContainerRestartPolicies_EnsuresRoutesAttachmentsA
1295 runtime.AssertNumberOfCalls(t, "EnsureContainerRestartPolicy", 3)1310 runtime.AssertNumberOfCalls(t, "EnsureContainerRestartPolicy", 3)
1296 }1311 }
1297 1312
1298-func TestService_EnsureManagedContainerRestartPolicies_ErrorIsNonFatal(t *testing.T) {1313+func TestService_EnsureManagedContainerRestartPolicies_ContinuesAndReturnsError(t *testing.T) {
1299 runtime := mocks.NewMockContainerRuntime(t)1314 runtime := mocks.NewMockContainerRuntime(t)
1300 envLoader := mocks.NewMockEnvLoader(t)1315 envLoader := mocks.NewMockEnvLoader(t)
1301 eventBus := mocks.NewMockEventPublisher(t)1316 eventBus := mocks.NewMockEventPublisher(t)
@@ -1311,12 +1326,23 @@ func TestService_EnsureManagedContainerRestartPolicies_ErrorIsNonFatal(t *testin
1311 domain.LabelDomain: "app.example.com",1326 domain.LabelDomain: "app.example.com",
1312 },1327 },
1313 }1328 }
1329+ routeContainer2 := &domain.Container{
1330+ ID: "route-2",
1331+ Name: "gordon-api.example.com",
1332+ Labels: map[string]string{
1333+ domain.LabelManaged: "true",
1334+ domain.LabelDomain: "api.example.com",
1335+ },
1336+ }
1314 1337
1315- runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{routeContainer}, nil).Once()1338+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{routeContainer, routeContainer2}, nil).Once()
1316 runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "route-1", domain.RestartPolicyAlways).Return(errors.New("update failed")).Once()1339 runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "route-1", domain.RestartPolicyAlways).Return(errors.New("update failed")).Once()
1340+ runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "route-2", domain.RestartPolicyAlways).Return(nil).Once()
1317 err := svc.EnsureManagedContainerRestartPolicies(ctx)1341 err := svc.EnsureManagedContainerRestartPolicies(ctx)
1318 1342
1319- require.NoError(t, err)1343+ require.Error(t, err)
1344+ assert.Contains(t, err.Error(), "route-1")
1345+ assert.Contains(t, err.Error(), "update failed")
1320 }1346 }
1321 1347
1322 func TestService_SyncContainers(t *testing.T) {1348 func TestService_SyncContainers(t *testing.T) {
@@ -1327,7 +1353,6 @@ func TestService_SyncContainers(t *testing.T) {
1327 svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)1353 svc := NewService(runtime, envLoader, eventBus, nil, Config{}, nil)
1328 ctx := testContext()1354 ctx := testContext()
1329 1355
1330- runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
1331 runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{1356 runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{
1332 {1357 {
1333 ID: "container-1",1358 ID: "container-1",
@@ -1535,8 +1560,6 @@ func TestService_Restart_WithAttachments(t *testing.T) {
1535 },1560 },
1536 },1561 },
1537 }, nil).Maybe()1562 }, nil).Maybe()
1538- runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
1539-
1540 runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil)1563 runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil)
1541 runtime.EXPECT().RestartContainer(mock.Anything, "container-attach-1").Return(nil)1564 runtime.EXPECT().RestartContainer(mock.Anything, "container-attach-1").Return(nil)
1542 runtime.EXPECT().RestartContainer(mock.Anything, "container-attach-2").Return(fmt.Errorf("boom"))1565 runtime.EXPECT().RestartContainer(mock.Anything, "container-attach-2").Return(fmt.Errorf("boom"))
@@ -1612,8 +1635,6 @@ func TestService_Restart_WithAttachments_ErrorsWhenPartiallyDeployed(t *testing.
1612 },1635 },
1613 },1636 },
1614 }, nil).Maybe()1637 }, nil).Maybe()
1615- runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
1616-
1617 err := svc.Restart(testContext(), "test.example.com", true)1638 err := svc.Restart(testContext(), "test.example.com", true)
1618 1639
1619 assert.Error(t, err)1640 assert.Error(t, err)
@@ -1659,8 +1680,6 @@ func TestService_Restart_WithAttachments_SucceedsWhenAllDeployed(t *testing.T) {
1659 },1680 },
1660 },1681 },
1661 }, nil).Maybe()1682 }, nil).Maybe()
1662- runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, mock.Anything, domain.RestartPolicyAlways).Return(nil).Maybe()
1663-
1664 runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil)1683 runtime.EXPECT().RestartContainer(mock.Anything, "container-123").Return(nil)
1665 runtime.EXPECT().RestartContainer(mock.Anything, "attach-456").Return(nil)1684 runtime.EXPECT().RestartContainer(mock.Anything, "attach-456").Return(nil)
1666 1685
@@ -2036,6 +2055,7 @@ func TestRewriteToLocalRegistry(t *testing.T) {
2036 2055
2037 func TestService_Deploy_InternalDeployForcesPull(t *testing.T) {2056 func TestService_Deploy_InternalDeployForcesPull(t *testing.T) {
2038 runtime := mocks.NewMockContainerRuntime(t)2057 runtime := mocks.NewMockContainerRuntime(t)
2058+ allowRestartPolicyEnsure(t, runtime)
2039 envLoader := mocks.NewMockEnvLoader(t)2059 envLoader := mocks.NewMockEnvLoader(t)
2040 eventBus := mocks.NewMockEventPublisher(t)2060 eventBus := mocks.NewMockEventPublisher(t)
2041 2061
@@ -2116,6 +2136,7 @@ func TestService_Deploy_InternalDeployForcesPull(t *testing.T) {
2116 2136
2117 func TestService_AutoStart_StartsNewContainers(t *testing.T) {2137 func TestService_AutoStart_StartsNewContainers(t *testing.T) {
2118 runtime := mocks.NewMockContainerRuntime(t)2138 runtime := mocks.NewMockContainerRuntime(t)
2139+ allowRestartPolicyEnsure(t, runtime)
2119 envLoader := mocks.NewMockEnvLoader(t)2140 envLoader := mocks.NewMockEnvLoader(t)
2120 eventBus := mocks.NewMockEventPublisher(t)2141 eventBus := mocks.NewMockEventPublisher(t)
2121 2142
@@ -2165,6 +2186,7 @@ func TestService_AutoStart_StartsNewContainers(t *testing.T) {
2165 2186
2166 func TestService_AutoStart_SkipsExistingContainers(t *testing.T) {2187 func TestService_AutoStart_SkipsExistingContainers(t *testing.T) {
2167 runtime := mocks.NewMockContainerRuntime(t)2188 runtime := mocks.NewMockContainerRuntime(t)
2189+ allowRestartPolicyEnsure(t, runtime)
2168 envLoader := mocks.NewMockEnvLoader(t)2190 envLoader := mocks.NewMockEnvLoader(t)
2169 eventBus := mocks.NewMockEventPublisher(t)2191 eventBus := mocks.NewMockEventPublisher(t)
2170 2192
@@ -2214,6 +2236,7 @@ func TestService_AutoStart_SkipsExistingContainers(t *testing.T) {
2214 2236
2215 func TestService_AutoStart_HandlesDeployErrors(t *testing.T) {2237 func TestService_AutoStart_HandlesDeployErrors(t *testing.T) {
2216 runtime := mocks.NewMockContainerRuntime(t)2238 runtime := mocks.NewMockContainerRuntime(t)
2239+ allowRestartPolicyEnsure(t, runtime)
2217 envLoader := mocks.NewMockEnvLoader(t)2240 envLoader := mocks.NewMockEnvLoader(t)
2218 eventBus := mocks.NewMockEventPublisher(t)2241 eventBus := mocks.NewMockEventPublisher(t)
2219 2242
@@ -2242,6 +2265,7 @@ func TestService_AutoStart_HandlesDeployErrors(t *testing.T) {
2242 2265
2243 func TestService_AutoStart_UsesInternalDeployContext(t *testing.T) {2266 func TestService_AutoStart_UsesInternalDeployContext(t *testing.T) {
2244 runtime := mocks.NewMockContainerRuntime(t)2267 runtime := mocks.NewMockContainerRuntime(t)
2268+ allowRestartPolicyEnsure(t, runtime)
2245 envLoader := mocks.NewMockEnvLoader(t)2269 envLoader := mocks.NewMockEnvLoader(t)
2246 eventBus := mocks.NewMockEventPublisher(t)2270 eventBus := mocks.NewMockEventPublisher(t)
2247 2271
@@ -2294,6 +2318,7 @@ func TestService_AutoStart_UsesInternalDeployContext(t *testing.T) {
2294 // until the new container is ready and traffic is switched.2318 // until the new container is ready and traffic is switched.
2295 func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) {2319 func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) {
2296 runtime := mocks.NewMockContainerRuntime(t)2320 runtime := mocks.NewMockContainerRuntime(t)
2321+ allowRestartPolicyEnsure(t, runtime)
2297 envLoader := mocks.NewMockEnvLoader(t)2322 envLoader := mocks.NewMockEnvLoader(t)
2298 eventBus := mocks.NewMockEventPublisher(t)2323 eventBus := mocks.NewMockEventPublisher(t)
2299 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)2324 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -2390,6 +2415,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) {
2390 // Running canonical containers are preserved (see NeverKillsRunningCanonical test).2415 // Running canonical containers are preserved (see NeverKillsRunningCanonical test).
2391 func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) {2416 func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) {
2392 runtime := mocks.NewMockContainerRuntime(t)2417 runtime := mocks.NewMockContainerRuntime(t)
2418+ allowRestartPolicyEnsure(t, runtime)
2393 envLoader := mocks.NewMockEnvLoader(t)2419 envLoader := mocks.NewMockEnvLoader(t)
2394 eventBus := mocks.NewMockEventPublisher(t)2420 eventBus := mocks.NewMockEventPublisher(t)
2395 2421
@@ -2470,6 +2496,7 @@ func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) {
2470 2496
2471 func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) {2497 func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) {
2472 runtime := mocks.NewMockContainerRuntime(t)2498 runtime := mocks.NewMockContainerRuntime(t)
2499+ allowRestartPolicyEnsure(t, runtime)
2473 envLoader := mocks.NewMockEnvLoader(t)2500 envLoader := mocks.NewMockEnvLoader(t)
2474 eventBus := mocks.NewMockEventPublisher(t)2501 eventBus := mocks.NewMockEventPublisher(t)
2475 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)2502 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -2542,6 +2569,7 @@ func TestService_Deploy_OrphanCleanupRemovesStaleNewContainer(t *testing.T) {
2542 2569
2543 func TestService_Deploy_TrackedTempContainerUsesAlternateTempName(t *testing.T) {2570 func TestService_Deploy_TrackedTempContainerUsesAlternateTempName(t *testing.T) {
2544 runtime := mocks.NewMockContainerRuntime(t)2571 runtime := mocks.NewMockContainerRuntime(t)
2572+ allowRestartPolicyEnsure(t, runtime)
2545 envLoader := mocks.NewMockEnvLoader(t)2573 envLoader := mocks.NewMockEnvLoader(t)
2546 eventBus := mocks.NewMockEventPublisher(t)2574 eventBus := mocks.NewMockEventPublisher(t)
2547 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)2575 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -2731,6 +2759,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) {
2731 // Verify that concurrent Deploy calls for the same domain are serialized2759 // Verify that concurrent Deploy calls for the same domain are serialized
2732 // and both succeed without container name conflicts.2760 // and both succeed without container name conflicts.
2733 runtime := mocks.NewMockContainerRuntime(t)2761 runtime := mocks.NewMockContainerRuntime(t)
2762+ allowRestartPolicyEnsure(t, runtime)
2734 envLoader := mocks.NewMockEnvLoader(t)2763 envLoader := mocks.NewMockEnvLoader(t)
2735 eventBus := mocks.NewMockEventPublisher(t)2764 eventBus := mocks.NewMockEventPublisher(t)
2736 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)2765 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -2841,6 +2870,7 @@ func TestService_Deploy_ConcurrentSameDomain(t *testing.T) {
2841 func TestService_Deploy_ContextCancellation(t *testing.T) {2870 func TestService_Deploy_ContextCancellation(t *testing.T) {
2842 // Verify that deploy lock acquisition respects context cancellation.2871 // Verify that deploy lock acquisition respects context cancellation.
2843 runtime := mocks.NewMockContainerRuntime(t)2872 runtime := mocks.NewMockContainerRuntime(t)
2873+ allowRestartPolicyEnsure(t, runtime)
2844 envLoader := mocks.NewMockEnvLoader(t)2874 envLoader := mocks.NewMockEnvLoader(t)
2845 eventBus := mocks.NewMockEventPublisher(t)2875 eventBus := mocks.NewMockEventPublisher(t)
2846 2876
@@ -2871,6 +2901,7 @@ func TestService_Deploy_ContextCancellation(t *testing.T) {
2871 // BEFORE the old container is stopped, preventing 503 errors during deployment.2901 // BEFORE the old container is stopped, preventing 503 errors during deployment.
2872 func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) {2902 func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) {
2873 runtime := mocks.NewMockContainerRuntime(t)2903 runtime := mocks.NewMockContainerRuntime(t)
2904+ allowRestartPolicyEnsure(t, runtime)
2874 envLoader := mocks.NewMockEnvLoader(t)2905 envLoader := mocks.NewMockEnvLoader(t)
2875 eventBus := mocks.NewMockEventPublisher(t)2906 eventBus := mocks.NewMockEventPublisher(t)
2876 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)2907 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -2951,6 +2982,7 @@ func TestService_Deploy_CacheInvalidationBeforeOldContainerStop(t *testing.T) {
2951 // when no cache invalidator is set (e.g., in tests or minimal configurations).2982 // when no cache invalidator is set (e.g., in tests or minimal configurations).
2952 func TestService_Deploy_NilCacheInvalidator(t *testing.T) {2983 func TestService_Deploy_NilCacheInvalidator(t *testing.T) {
2953 runtime := mocks.NewMockContainerRuntime(t)2984 runtime := mocks.NewMockContainerRuntime(t)
2985+ allowRestartPolicyEnsure(t, runtime)
2954 envLoader := mocks.NewMockEnvLoader(t)2986 envLoader := mocks.NewMockEnvLoader(t)
2955 eventBus := mocks.NewMockEventPublisher(t)2987 eventBus := mocks.NewMockEventPublisher(t)
2956 2988
@@ -3000,6 +3032,7 @@ func TestService_Deploy_NilCacheInvalidator(t *testing.T) {
3000 3032
3001 func TestService_Deploy_SkipsRedundantDeploy(t *testing.T) {3033 func TestService_Deploy_SkipsRedundantDeploy(t *testing.T) {
3002 runtime := mocks.NewMockContainerRuntime(t)3034 runtime := mocks.NewMockContainerRuntime(t)
3035+ allowRestartPolicyEnsure(t, runtime)
3003 envLoader := mocks.NewMockEnvLoader(t)3036 envLoader := mocks.NewMockEnvLoader(t)
3004 eventBus := mocks.NewMockEventPublisher(t)3037 eventBus := mocks.NewMockEventPublisher(t)
3005 3038
@@ -3060,6 +3093,7 @@ func TestService_Deploy_SkipsRedundantDeploy(t *testing.T) {
3060 3093
3061 func TestService_Deploy_SkipsRedundantDeploy_WhenTrackedImageIDMissing(t *testing.T) {3094 func TestService_Deploy_SkipsRedundantDeploy_WhenTrackedImageIDMissing(t *testing.T) {
3062 runtime := mocks.NewMockContainerRuntime(t)3095 runtime := mocks.NewMockContainerRuntime(t)
3096+ allowRestartPolicyEnsure(t, runtime)
3063 envLoader := mocks.NewMockEnvLoader(t)3097 envLoader := mocks.NewMockEnvLoader(t)
3064 eventBus := mocks.NewMockEventPublisher(t)3098 eventBus := mocks.NewMockEventPublisher(t)
3065 3099
@@ -3121,6 +3155,7 @@ func TestService_Deploy_SkipsRedundantDeploy_WhenTrackedImageIDMissing(t *testin
3121 3155
3122 func TestService_Deploy_DoesNotSkipWhenImageIDDiffers(t *testing.T) {3156 func TestService_Deploy_DoesNotSkipWhenImageIDDiffers(t *testing.T) {
3123 runtime := mocks.NewMockContainerRuntime(t)3157 runtime := mocks.NewMockContainerRuntime(t)
3158+ allowRestartPolicyEnsure(t, runtime)
3124 envLoader := mocks.NewMockEnvLoader(t)3159 envLoader := mocks.NewMockEnvLoader(t)
3125 eventBus := mocks.NewMockEventPublisher(t)3160 eventBus := mocks.NewMockEventPublisher(t)
3126 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)3161 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -3223,6 +3258,7 @@ func TestSyncContainersRebuildsAttachmentMap(t *testing.T) {
3223 3258
3224 func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) {3259 func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) {
3225 runtime := mocks.NewMockContainerRuntime(t)3260 runtime := mocks.NewMockContainerRuntime(t)
3261+ allowRestartPolicyEnsure(t, runtime)
3226 envLoader := mocks.NewMockEnvLoader(t)3262 envLoader := mocks.NewMockEnvLoader(t)
3227 eventBus := mocks.NewMockEventPublisher(t)3263 eventBus := mocks.NewMockEventPublisher(t)
3228 3264
@@ -3286,6 +3322,7 @@ func TestService_Deploy_SkipRedundantDeploy_ContainerNotRunning(t *testing.T) {
3286 3322
3287 func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) {3323 func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) {
3288 runtime := mocks.NewMockContainerRuntime(t)3324 runtime := mocks.NewMockContainerRuntime(t)
3325+ allowRestartPolicyEnsure(t, runtime)
3289 envLoader := mocks.NewMockEnvLoader(t)3326 envLoader := mocks.NewMockEnvLoader(t)
3290 eventBus := mocks.NewMockEventPublisher(t)3327 eventBus := mocks.NewMockEventPublisher(t)
3291 3328
@@ -3344,6 +3381,7 @@ func TestService_Deploy_DoesNotSkip_WhenEnvHashDiffers(t *testing.T) {
3344 3381
3345 func TestService_Deploy_DoesNotSkip_WhenEnvHashMissing(t *testing.T) {3382 func TestService_Deploy_DoesNotSkip_WhenEnvHashMissing(t *testing.T) {
3346 runtime := mocks.NewMockContainerRuntime(t)3383 runtime := mocks.NewMockContainerRuntime(t)
3384+ allowRestartPolicyEnsure(t, runtime)
3347 envLoader := mocks.NewMockEnvLoader(t)3385 envLoader := mocks.NewMockEnvLoader(t)
3348 eventBus := mocks.NewMockEventPublisher(t)3386 eventBus := mocks.NewMockEventPublisher(t)
3349 3387
@@ -3470,6 +3508,7 @@ func TestService_attachmentEnvDrifted_ReturnsTrueWhenHashMissing(t *testing.T) {
3470 // disallows creating a container with the same name as an existing running container.3508 // disallows creating a container with the same name as an existing running container.
3471 func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) {3509 func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) {
3472 runtime := mocks.NewMockContainerRuntime(t)3510 runtime := mocks.NewMockContainerRuntime(t)
3511+ allowRestartPolicyEnsure(t, runtime)
3473 envLoader := mocks.NewMockEnvLoader(t)3512 envLoader := mocks.NewMockEnvLoader(t)
3474 eventBus := mocks.NewMockEventPublisher(t)3513 eventBus := mocks.NewMockEventPublisher(t)
3475 3514
@@ -3566,6 +3605,7 @@ func TestService_Deploy_OrphanCleanup_NeverKillsRunningCanonical(t *testing.T) {
3566 // and the failed new container is cleaned up.3605 // and the failed new container is cleaned up.
3567 func TestService_Deploy_RollbackOnPostSwitchCrash(t *testing.T) {3606 func TestService_Deploy_RollbackOnPostSwitchCrash(t *testing.T) {
3568 runtime := mocks.NewMockContainerRuntime(t)3607 runtime := mocks.NewMockContainerRuntime(t)
3608+ allowRestartPolicyEnsure(t, runtime)
3569 envLoader := mocks.NewMockEnvLoader(t)3609 envLoader := mocks.NewMockEnvLoader(t)
3570 eventBus := mocks.NewMockEventPublisher(t)3610 eventBus := mocks.NewMockEventPublisher(t)
3571 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)3611 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -3660,6 +3700,7 @@ func TestService_Deploy_RollbackOnPostSwitchCrash(t *testing.T) {
3660 // container is finalized (stopped and removed).3700 // container is finalized (stopped and removed).
3661 func TestService_Deploy_StabilizationSuccess(t *testing.T) {3701 func TestService_Deploy_StabilizationSuccess(t *testing.T) {
3662 runtime := mocks.NewMockContainerRuntime(t)3702 runtime := mocks.NewMockContainerRuntime(t)
3703+ allowRestartPolicyEnsure(t, runtime)
3663 envLoader := mocks.NewMockEnvLoader(t)3704 envLoader := mocks.NewMockEnvLoader(t)
3664 eventBus := mocks.NewMockEventPublisher(t)3705 eventBus := mocks.NewMockEventPublisher(t)
3665 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)3706 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -3748,6 +3789,7 @@ func TestService_Deploy_StabilizationSuccess(t *testing.T) {
3748 // Uses testify's NotBefore() to enforce mock call ordering.3789 // Uses testify's NotBefore() to enforce mock call ordering.
3749 func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T) {3790 func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T) {
3750 runtime := mocks.NewMockContainerRuntime(t)3791 runtime := mocks.NewMockContainerRuntime(t)
3792+ allowRestartPolicyEnsure(t, runtime)
3751 envLoader := mocks.NewMockEnvLoader(t)3793 envLoader := mocks.NewMockEnvLoader(t)
3752 eventBus := mocks.NewMockEventPublisher(t)3794 eventBus := mocks.NewMockEventPublisher(t)
3753 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)3795 cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
@@ -3844,6 +3886,7 @@ func TestService_Deploy_ZeroDowntime_OldNeverStoppedBeforeNewReady(t *testing.T)
3844 // the old container remains as the tracked container.3886 // the old container remains as the tracked container.
3845 func TestService_Deploy_ZeroDowntime_ReadinessFailure_OldUntouched(t *testing.T) {3887 func TestService_Deploy_ZeroDowntime_ReadinessFailure_OldUntouched(t *testing.T) {
3846 runtime := mocks.NewMockContainerRuntime(t)3888 runtime := mocks.NewMockContainerRuntime(t)
3889+ allowRestartPolicyEnsure(t, runtime)
3847 envLoader := mocks.NewMockEnvLoader(t)3890 envLoader := mocks.NewMockEnvLoader(t)
3848 eventBus := mocks.NewMockEventPublisher(t)3891 eventBus := mocks.NewMockEventPublisher(t)
3849 3892
@@ -3934,6 +3977,7 @@ func TestService_Deploy_ZeroDowntime_ReadinessFailure_OldUntouched(t *testing.T)
3934 // finalize/drain logic being triggered.3977 // finalize/drain logic being triggered.
3935 func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) {3978 func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) {
3936 runtime := mocks.NewMockContainerRuntime(t)3979 runtime := mocks.NewMockContainerRuntime(t)
3980+ allowRestartPolicyEnsure(t, runtime)
3937 envLoader := mocks.NewMockEnvLoader(t)3981 envLoader := mocks.NewMockEnvLoader(t)
3938 eventBus := mocks.NewMockEventPublisher(t)3982 eventBus := mocks.NewMockEventPublisher(t)
3939 3983
@@ -4010,6 +4054,7 @@ func TestService_Deploy_ZeroDowntime_NoExisting_FirstDeploy(t *testing.T) {
4010 4054
4011 func TestService_Deploy_PropagatesImageLabelsToContainerConfig(t *testing.T) {4055 func TestService_Deploy_PropagatesImageLabelsToContainerConfig(t *testing.T) {
4012 runtime := mocks.NewMockContainerRuntime(t)4056 runtime := mocks.NewMockContainerRuntime(t)
4057+ allowRestartPolicyEnsure(t, runtime)
4013 envLoader := mocks.NewMockEnvLoader(t)4058 envLoader := mocks.NewMockEnvLoader(t)
4014 eventBus := mocks.NewMockEventPublisher(t)4059 eventBus := mocks.NewMockEventPublisher(t)
4015 4060
@@ -4267,7 +4312,7 @@ func TestDeployAttachedService_SetsAliasOnContainerConfig(t *testing.T) {
4267 assert.Equal(t, []string{"postgres"}, cfg.Aliases, "attachment container must have network aliases for DNS resolution")4312 assert.Equal(t, []string{"postgres"}, cfg.Aliases, "attachment container must have network aliases for DNS resolution")
4268 assert.Equal(t, "postgres", cfg.Hostname)4313 assert.Equal(t, "postgres", cfg.Hostname)
4269 assert.Equal(t, networkName, cfg.NetworkMode)4314 assert.Equal(t, networkName, cfg.NetworkMode)
4270- assert.Equal(t, domain.RestartPolicyAlways, cfg.RestartPolicy)4315+ assert.Empty(t, cfg.RestartPolicy)
4271 }).4316 }).
4272 Return(&domain.Container{ID: "pg-container-1", Name: containerName}, nil)4317 Return(&domain.Container{ID: "pg-container-1", Name: containerName}, nil)
4273 4318
@@ -4280,6 +4325,7 @@ func TestDeployAttachedService_SetsAliasOnContainerConfig(t *testing.T) {
4280 runtime.EXPECT().GetContainerHealthStatus(mock.Anything, "pg-container-1").Return("", false, nil)4325 runtime.EXPECT().GetContainerHealthStatus(mock.Anything, "pg-container-1").Return("", false, nil)
4281 // TCP probe: resolve endpoint fails -> falls back to delay4326 // TCP probe: resolve endpoint fails -> falls back to delay
4282 runtime.EXPECT().GetContainerNetworkInfo(mock.Anything, "pg-container-1").Return("", 0, errors.New("no network"))4327 runtime.EXPECT().GetContainerNetworkInfo(mock.Anything, "pg-container-1").Return("", 0, errors.New("no network"))
4328+ runtime.EXPECT().EnsureContainerRestartPolicy(mock.Anything, "pg-container-1", domain.RestartPolicyAlways).Return(nil).Once()
4283 4329
4284 err := svc.deployAttachedService(ctx, ownerDomain, serviceImage, networkName)4330 err := svc.deployAttachedService(ctx, ownerDomain, serviceImage, networkName)
4285 require.NoError(t, err)4331 require.NoError(t, err)
@@ -4405,10 +4451,10 @@ func TestService_BuildContainerConfig_CompatSecurityProfileDefault(t *testing.T)
4405 assert.Nil(t, cfg.CapAdd)4451 assert.Nil(t, cfg.CapAdd)
4406 }4452 }
4407 4453
4408-func TestService_BuildContainerConfig_SetsRestartPolicyAlways(t *testing.T) {4454+func TestService_BuildContainerConfig_OmitsRestartPolicyBeforeReadiness(t *testing.T) {
4409 svc := NewService(nil, nil, nil, nil, Config{}, nil)4455 svc := NewService(nil, nil, nil, nil, Config{}, nil)
4410 4456
4411 cfg := svc.buildContainerConfig(containerConfigInput{Domain: "app.example.com", Image: "app:latest", ImageRef: "app:latest"})4457 cfg := svc.buildContainerConfig(containerConfigInput{Domain: "app.example.com", Image: "app:latest", ImageRef: "app:latest"})
4412 4458
4413- assert.Equal(t, domain.RestartPolicyAlways, cfg.RestartPolicy)4459+ assert.Empty(t, cfg.RestartPolicy)
4414 }4460 }

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