1package deployment
2
3import (
4 "context"
5 "fmt"
6 "sort"
7
8 "github.com/bnema/zerowrap"
9
10 "github.com/bnema/gordon/internal/domain"
11)
12
13// reconcileRemovalsForDeploy journals and applies the removal of services
14// the new revision no longer declares. A failure marks the operation
15// failed and aborts before any new state is published.
16func (s *Service) reconcileRemovalsForDeploy(ctx context.Context, app string, active domain.AppActive, pinned []pinnedService, op *domain.AppOperation, result *DeployResult, log zerowrap.Logger) error {
17 removalSteps, removed, warnings, err := s.reconcileRemovedServices(ctx, app, active, pinned)
18 op.Warnings = append(op.Warnings, journalWarnings(warnings)...)
19 result.CleanupWarnings = append(result.CleanupWarnings, warnings...)
20 if err != nil {
21 op.Steps = append(op.Steps, removalSteps...)
22 op.Outcome = domain.AppOutcomeFailed
23 if saveErr := s.deps.State.SaveOperation(ctx, *op); saveErr != nil {
24 log.Warn().Err(saveErr).Msg("deployment: failed to record removal failure")
25 }
26 return err
27 }
28 if len(removalSteps) == 0 {
29 return nil
30 }
31 op.Steps = append(op.Steps, removalSteps...)
32 result.Removed = removed
33 if saveErr := s.deps.State.SaveOperation(ctx, *op); saveErr != nil {
34 log.Warn().Err(saveErr).Msg("deployment: failed to record service removals")
35 }
36 return nil
37}
38
39// reconcileRemovedServices withdraws and retires every ACTIVE service the
40// new revision no longer declares. Traffic is withdrawn first (fail
41// closed), then the exact container is inhibited, stopped, and removed
42// with its data retained, its backend claims released, and its ACTIVE
43// entry deleted and persisted before its recovery inhibition is cleared. A failure leaves the remaining services untouched and
44// stops the deploy before any new state is published.
45func (s *Service) reconcileRemovedServices(ctx context.Context, app string, active domain.AppActive, pinned []pinnedService) ([]domain.AppOperationStep, []string, []CleanupWarning, error) {
46 desired := make(map[string]struct{}, len(pinned))
47 for _, p := range pinned {
48 desired[p.name] = struct{}{}
49 }
50 var names []string
51 for name := range active.Services {
52 if _, ok := desired[name]; !ok {
53 names = append(names, name)
54 }
55 }
56 sort.Strings(names)
57
58 steps := make([]domain.AppOperationStep, 0, len(names))
59 var cleanupWarnings []CleanupWarning
60 for _, name := range names {
61 eff := active.Services[name]
62 step, warnings, err := s.retireRemovedService(ctx, app, name, eff)
63 steps = append(steps, step)
64 cleanupWarnings = append(cleanupWarnings, warnings...)
65 if err != nil {
66 return steps, names, cleanupWarnings, err
67 }
68 // Persist the removal before dropping the recovery inhibition: an
69 // ACTIVE record that still lists the service without its
70 // inhibition would let recovery recreate a removed service.
71 delete(active.Services, name)
72 if err := s.deps.State.SaveActive(ctx, active); err != nil {
73 return steps, names, cleanupWarnings, fmt.Errorf("deployment: persist service removal %q: %w", name, err)
74 }
75 if err := s.clearRecoveryInhibition(ctx, app, name, eff.Container); err != nil {
76 cleanupWarnings = append(cleanupWarnings, CleanupWarning{
77 Service: name, Leftover: eff.Container, Detail: "clear recovery inhibition: " + err.Error(),
78 })
79 }
80 }
81 if len(names) == 0 {
82 return nil, nil, nil, nil
83 }
84 return steps, names, cleanupWarnings, nil
85}
86
87// retireRemovedService withdraws one removed service and removes its exact
88// container, releasing its claims only after withdrawal succeeded.
89func (s *Service) retireRemovedService(ctx context.Context, app, name string, eff domain.AppEffectiveService) (domain.AppOperationStep, []CleanupWarning, error) {
90 step := domain.AppOperationStep{
91 ID: "service." + name + ".remove",
92 State: domain.AppStepPending,
93 Service: name,
94 Before: eff.Container,
95 }
96 fail := func(err error) (domain.AppOperationStep, []CleanupWarning, error) {
97 step.State = domain.AppStepFailed
98 step.Error = err.Error()
99 return step, nil, err
100 }
101 if err := s.withdrawForRecovery(ctx, app, name); err != nil {
102 return fail(err)
103 }
104 if eff.Container != "" {
105 if err := s.inhibitRecovery(ctx, app, name, eff.Container, "removed", ""); err != nil {
106 return fail(err)
107 }
108 // The inhibition stays until the caller persisted the removal.
109 retired := s.retireContainer(ctx, app, retireOptions{
110 Service: name, Grace: serviceStopGrace(eff),
111 }, eff.Container)
112 if !retired.Gone {
113 return fail(fmt.Errorf("deployment: remove %s/%s container %s: %s", app, name, eff.Container, cleanupDetail(retired)))
114 }
115 step.State = domain.AppStepSucceeded
116 step.After = ""
117 return step, retired.Warnings, nil
118 }
119 step.State = domain.AppStepSucceeded
120 step.After = ""
121 return step, nil, nil
122}