detached 8b31c1c

fix(preview): propagate cleanup errors and defer container removal

cleanupOrphanDomainResources now returns an error. If resource cleanup
fails, the container is not removed — it stays stopped and will be
rediscovered on the next GC scan for retry.
commit
8b31c1c17e38858a9e1d35417b97b4456432d20d
author
brice · · 2026-03-25 08:00 UTC
parent
b32e7e8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +19 -4
M internal/app/run.go +19 -4
@@ -652,7 +652,11 @@ func gcOrphanedPreviews(ctx context.Context, svc *services, previewConfig domain
652652 }
653653
654654 if orphanDomain != "" {
655- cleanupOrphanDomainResources(ctx, svc, orphanDomain)
655+ if err := cleanupOrphanDomainResources(ctx, svc, orphanDomain); err != nil {
656+ log.Warn().Err(err).Str("container", c.Name).Str("domain", orphanDomain).
657+ Msg("orphan resource cleanup failed, deferring container removal to next scan")
658+ continue
659+ }
656660 }
657661
658662 if err := svc.runtime.RemoveContainer(ctx, c.Name, true); err != nil {
@@ -663,20 +667,27 @@ func gcOrphanedPreviews(ctx context.Context, svc *services, previewConfig domain
663667 }
664668 }
665669
666-// cleanupOrphanDomainResources removes volumes, network, and route for an orphaned preview domain.
667-func cleanupOrphanDomainResources(ctx context.Context, svc *services, orphanDomain string) {
670+// cleanupOrphanDomainResources removes volumes, network, and route for an
671+// orphaned preview domain. Returns an error if any step fails so the caller
672+// can defer container removal until the next scan.
673+func cleanupOrphanDomainResources(ctx context.Context, svc *services, orphanDomain string) error {
668674 log := zerowrap.FromCtx(ctx)
669675 domainSanitized := strings.ReplaceAll(orphanDomain, ".", "-")
676+ var cleanupErr error
670677
671678 // Remove volumes matching domain prefix (convention: {volPrefix}-{domain-sanitized}-).
672679 _, volPrefix, _ := svc.configSvc.GetVolumeConfig()
673680 fullVolumePrefix := volPrefix + "-" + domainSanitized + "-"
674681 volumes, err := svc.runtime.ListVolumes(ctx)
675- if err == nil {
682+ if err != nil {
683+ log.Warn().Err(err).Msg("failed to list volumes for orphan cleanup")
684+ cleanupErr = err
685+ } else {
676686 for _, v := range volumes {
677687 if strings.HasPrefix(v.Name, fullVolumePrefix) {
678688 if err := svc.runtime.RemoveVolume(ctx, v.Name, true); err != nil {
679689 log.Warn().Err(err).Str("volume", v.Name).Msg("failed to remove orphan volume")
690+ cleanupErr = err
680691 }
681692 }
682693 }
@@ -687,15 +698,19 @@ func cleanupOrphanDomainResources(ctx context.Context, svc *services, orphanDoma
687698 networkName := networkPrefix + "-" + domainSanitized
688699 if err := svc.runtime.RemoveNetwork(ctx, networkName); err != nil {
689700 log.Warn().Err(err).Str("network", networkName).Msg("failed to remove orphan network")
701+ cleanupErr = err
690702 }
691703
692704 // Remove route and invalidate proxy.
693705 if err := svc.configSvc.RemoveRoute(ctx, orphanDomain); err != nil {
694706 if !errors.Is(err, domain.ErrRouteNotFound) {
695707 log.Warn().Err(err).Str("domain", orphanDomain).Msg("failed to remove orphan route")
708+ cleanupErr = err
696709 }
697710 }
698711 svc.proxySvc.InvalidateTarget(ctx, orphanDomain)
712+
713+ return cleanupErr
699714 }
700715
701716 // injectTelemetryMetrics creates and injects OTel metrics into services when
@@ -652,7 +652,11 @@ func gcOrphanedPreviews(ctx context.Context, svc *services, previewConfig domain
652 }652 }
653 653
654 if orphanDomain != "" {654 if orphanDomain != "" {
655- cleanupOrphanDomainResources(ctx, svc, orphanDomain)655+ if err := cleanupOrphanDomainResources(ctx, svc, orphanDomain); err != nil {
656+ log.Warn().Err(err).Str("container", c.Name).Str("domain", orphanDomain).
657+ Msg("orphan resource cleanup failed, deferring container removal to next scan")
658+ continue
659+ }
656 }660 }
657 661
658 if err := svc.runtime.RemoveContainer(ctx, c.Name, true); err != nil {662 if err := svc.runtime.RemoveContainer(ctx, c.Name, true); err != nil {
@@ -663,20 +667,27 @@ func gcOrphanedPreviews(ctx context.Context, svc *services, previewConfig domain
663 }667 }
664 }668 }
665 669
666-// cleanupOrphanDomainResources removes volumes, network, and route for an orphaned preview domain.670+// cleanupOrphanDomainResources removes volumes, network, and route for an
667-func cleanupOrphanDomainResources(ctx context.Context, svc *services, orphanDomain string) {671+// orphaned preview domain. Returns an error if any step fails so the caller
672+// can defer container removal until the next scan.
673+func cleanupOrphanDomainResources(ctx context.Context, svc *services, orphanDomain string) error {
668 log := zerowrap.FromCtx(ctx)674 log := zerowrap.FromCtx(ctx)
669 domainSanitized := strings.ReplaceAll(orphanDomain, ".", "-")675 domainSanitized := strings.ReplaceAll(orphanDomain, ".", "-")
676+ var cleanupErr error
670 677
671 // Remove volumes matching domain prefix (convention: {volPrefix}-{domain-sanitized}-).678 // Remove volumes matching domain prefix (convention: {volPrefix}-{domain-sanitized}-).
672 _, volPrefix, _ := svc.configSvc.GetVolumeConfig()679 _, volPrefix, _ := svc.configSvc.GetVolumeConfig()
673 fullVolumePrefix := volPrefix + "-" + domainSanitized + "-"680 fullVolumePrefix := volPrefix + "-" + domainSanitized + "-"
674 volumes, err := svc.runtime.ListVolumes(ctx)681 volumes, err := svc.runtime.ListVolumes(ctx)
675- if err == nil {682+ if err != nil {
683+ log.Warn().Err(err).Msg("failed to list volumes for orphan cleanup")
684+ cleanupErr = err
685+ } else {
676 for _, v := range volumes {686 for _, v := range volumes {
677 if strings.HasPrefix(v.Name, fullVolumePrefix) {687 if strings.HasPrefix(v.Name, fullVolumePrefix) {
678 if err := svc.runtime.RemoveVolume(ctx, v.Name, true); err != nil {688 if err := svc.runtime.RemoveVolume(ctx, v.Name, true); err != nil {
679 log.Warn().Err(err).Str("volume", v.Name).Msg("failed to remove orphan volume")689 log.Warn().Err(err).Str("volume", v.Name).Msg("failed to remove orphan volume")
690+ cleanupErr = err
680 }691 }
681 }692 }
682 }693 }
@@ -687,15 +698,19 @@ func cleanupOrphanDomainResources(ctx context.Context, svc *services, orphanDoma
687 networkName := networkPrefix + "-" + domainSanitized698 networkName := networkPrefix + "-" + domainSanitized
688 if err := svc.runtime.RemoveNetwork(ctx, networkName); err != nil {699 if err := svc.runtime.RemoveNetwork(ctx, networkName); err != nil {
689 log.Warn().Err(err).Str("network", networkName).Msg("failed to remove orphan network")700 log.Warn().Err(err).Str("network", networkName).Msg("failed to remove orphan network")
701+ cleanupErr = err
690 }702 }
691 703
692 // Remove route and invalidate proxy.704 // Remove route and invalidate proxy.
693 if err := svc.configSvc.RemoveRoute(ctx, orphanDomain); err != nil {705 if err := svc.configSvc.RemoveRoute(ctx, orphanDomain); err != nil {
694 if !errors.Is(err, domain.ErrRouteNotFound) {706 if !errors.Is(err, domain.ErrRouteNotFound) {
695 log.Warn().Err(err).Str("domain", orphanDomain).Msg("failed to remove orphan route")707 log.Warn().Err(err).Str("domain", orphanDomain).Msg("failed to remove orphan route")
708+ cleanupErr = err
696 }709 }
697 }710 }
698 svc.proxySvc.InvalidateTarget(ctx, orphanDomain)711 svc.proxySvc.InvalidateTarget(ctx, orphanDomain)
712+
713+ return cleanupErr
699 }714 }
700 715
701 // injectTelemetryMetrics creates and injects OTel metrics into services when716 // injectTelemetryMetrics creates and injects OTel metrics into services when

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