| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "log" |
| 6 | "strings" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/release" |
| 10 | ) |
| 11 | |
| 12 | // evidenceFunc answers the empty-bucket tripwire (temporary static sites spec |
| 13 | // §2): does the apps/ listing hold at least one key that is not a trash |
| 14 | // marker? Every branch that reasons from absence to "the purge finished" asks |
| 15 | // it first, and an empty or failed listing is "no evidence". |
| 16 | type evidenceFunc func(ctx context.Context) bool |
| 17 | |
| 18 | // isTrashMarkerKey reports whether key is some app's trash marker, |
| 19 | // apps/<sqid>/<app>/deleted.json exactly. |
| 20 | func isTrashMarkerKey(key string) bool { |
| 21 | parts := strings.SplitN(key, "/", 4) |
| 22 | return len(parts) == 4 && parts[0] == "apps" && key == release.AppTombstoneKey(parts[1], parts[2]) |
| 23 | } |
| 24 | |
| 25 | // bucketEvidence is the tripwire over a listing already taken. Markers do not |
| 26 | // count: they are the one thing this feature writes into a bucket that holds |
| 27 | // nothing else, so a wrong, empty bucket would otherwise vouch for itself |
| 28 | // with the pass's own markers (astra 5.2). |
| 29 | func bucketEvidence(keys []string) bool { |
| 30 | for _, key := range keys { |
| 31 | if !isTrashMarkerKey(key) { |
| 32 | return true |
| 33 | } |
| 34 | } |
| 35 | return false |
| 36 | } |
| 37 | |
| 38 | // listEvidence lists apps/ once. listed is false when the listing failed; |
| 39 | // evidence is the tripwire's answer, false for a failed listing too. |
| 40 | func (s *Server) listEvidence(ctx context.Context) (keys []string, listed, evidence bool) { |
| 41 | if s.cfg.Releases == nil { |
| 42 | return nil, false, false |
| 43 | } |
| 44 | keys, err := s.cfg.Releases.List(ctx, "apps/") |
| 45 | if err != nil { |
| 46 | log.Printf("console: list apps/ for the empty-bucket tripwire: %v", err) |
| 47 | return nil, false, false |
| 48 | } |
| 49 | return keys, true, bucketEvidence(keys) |
| 50 | } |
| 51 | |
| 52 | // doorEvidence is the evidenceFunc a door passes to settleApp: it lists only |
| 53 | // if settleApp actually reaches an absence branch. |
| 54 | func (s *Server) doorEvidence(ctx context.Context) bool { |
| 55 | _, _, ev := s.listEvidence(ctx) |
| 56 | return ev |
| 57 | } |
| 58 | |
| 59 | // settleApp brings one app's row into line with its trash marker before any |
| 60 | // transition acts (spec §2 "Every transition settles first", §6). It is |
| 61 | // MaterializeApps for one app, plus the restoring cases. The CALLER holds |
| 62 | // s.lockApp(sqid, app), and re-reads the row afterwards: what it listed |
| 63 | // before the lock is only a hint. |
| 64 | // |
| 65 | // marker, row live -> trashed with the marker's clock (and the |
| 66 | // 3-day window if temporary, TrashAppRow) |
| 67 | // marker, row restoring -> restore step 4 failed: restoring = 0, the |
| 68 | // app stays trashed with its deadline |
| 69 | // no marker, row trashed, keys -> restored elsewhere, or step 5 failed: |
| 70 | // FinishRestoreRow |
| 71 | // no marker, row trashed, none -> purged elsewhere: PurgeAppRow, only if |
| 72 | // the tripwire finds evidence |
| 73 | // no marker, row live -> nothing |
| 74 | // |
| 75 | // An unreadable marker is an error and changes nothing. |
| 76 | func (s *Server) settleApp(ctx context.Context, accountID int64, sqid, app string, now time.Time, evidence evidenceFunc) error { |
| 77 | if s.cfg.Releases == nil { |
| 78 | return nil |
| 79 | } |
| 80 | life, err := s.store.AppLife(accountID, app) |
| 81 | if err != nil || life == nil { |
| 82 | return err |
| 83 | } |
| 84 | tomb, err := release.ReadAppTombstone(ctx, s.cfg.Releases, sqid, app) |
| 85 | if err != nil { |
| 86 | return err |
| 87 | } |
| 88 | if tomb != nil { |
| 89 | switch { |
| 90 | case !life.Trashed(): |
| 91 | if err := s.store.TrashAppRowContext(ctx, accountID, app, tomb.DeletedAt); err != nil { |
| 92 | return err |
| 93 | } |
| 94 | log.Printf("console: %s/%s is in the trash in the bucket; marking it trashed here", sqid, app) |
| 95 | case life.Restoring: |
| 96 | if _, err := s.store.SetRestoringContext(ctx, accountID, app, false); err != nil { |
| 97 | return err |
| 98 | } |
| 99 | log.Printf("console: %s/%s: a restore did not remove the trash marker; it stays in the trash", sqid, app) |
| 100 | } |
| 101 | return nil |
| 102 | } |
| 103 | if !life.Trashed() { |
| 104 | return nil |
| 105 | } |
| 106 | keys, err := s.cfg.Releases.List(ctx, release.AppPrefix(sqid, app)) |
| 107 | if err != nil { |
| 108 | return err |
| 109 | } |
| 110 | if len(keys) > 0 { |
| 111 | if err := s.store.FinishRestoreRow(accountID, app, now); err != nil { |
| 112 | return err |
| 113 | } |
| 114 | log.Printf("console: %s/%s has no trash marker and still has objects; restoring it here", sqid, app) |
| 115 | return nil |
| 116 | } |
| 117 | if !evidence(ctx) { |
| 118 | log.Printf("console: %s/%s has no trash marker and no objects, but the apps/ listing holds nothing else either; "+ |
| 119 | "leaving the row. Is CARLOS_DEPLOYMENT_BUCKET pointing at the right bucket?", sqid, app) |
| 120 | return nil |
| 121 | } |
| 122 | if err := s.store.PurgeAppRow(accountID, app); err != nil { |
| 123 | return err |
| 124 | } |
| 125 | log.Printf("console: %s/%s is gone from the bucket; releasing the name here", sqid, app) |
| 126 | return nil |
| 127 | } |
| 128 | |