package console import ( "context" "log" "strings" "time" "github.com/carlosframework/platform/internal/release" ) // evidenceFunc answers the empty-bucket tripwire (temporary static sites spec // §2): does the apps/ listing hold at least one key that is not a trash // marker? Every branch that reasons from absence to "the purge finished" asks // it first, and an empty or failed listing is "no evidence". type evidenceFunc func(ctx context.Context) bool // isTrashMarkerKey reports whether key is some app's trash marker, // apps///deleted.json exactly. func isTrashMarkerKey(key string) bool { parts := strings.SplitN(key, "/", 4) return len(parts) == 4 && parts[0] == "apps" && key == release.AppTombstoneKey(parts[1], parts[2]) } // bucketEvidence is the tripwire over a listing already taken. Markers do not // count: they are the one thing this feature writes into a bucket that holds // nothing else, so a wrong, empty bucket would otherwise vouch for itself // with the pass's own markers (astra 5.2). func bucketEvidence(keys []string) bool { for _, key := range keys { if !isTrashMarkerKey(key) { return true } } return false } // listEvidence lists apps/ once. listed is false when the listing failed; // evidence is the tripwire's answer, false for a failed listing too. func (s *Server) listEvidence(ctx context.Context) (keys []string, listed, evidence bool) { if s.cfg.Releases == nil { return nil, false, false } keys, err := s.cfg.Releases.List(ctx, "apps/") if err != nil { log.Printf("console: list apps/ for the empty-bucket tripwire: %v", err) return nil, false, false } return keys, true, bucketEvidence(keys) } // doorEvidence is the evidenceFunc a door passes to settleApp: it lists only // if settleApp actually reaches an absence branch. func (s *Server) doorEvidence(ctx context.Context) bool { _, _, ev := s.listEvidence(ctx) return ev } // settleApp brings one app's row into line with its trash marker before any // transition acts (spec §2 "Every transition settles first", §6). It is // MaterializeApps for one app, plus the restoring cases. The CALLER holds // s.lockApp(sqid, app), and re-reads the row afterwards: what it listed // before the lock is only a hint. // // marker, row live -> trashed with the marker's clock (and the // 3-day window if temporary, TrashAppRow) // marker, row restoring -> restore step 4 failed: restoring = 0, the // app stays trashed with its deadline // no marker, row trashed, keys -> restored elsewhere, or step 5 failed: // FinishRestoreRow // no marker, row trashed, none -> purged elsewhere: PurgeAppRow, only if // the tripwire finds evidence // no marker, row live -> nothing // // An unreadable marker is an error and changes nothing. func (s *Server) settleApp(ctx context.Context, accountID int64, sqid, app string, now time.Time, evidence evidenceFunc) error { if s.cfg.Releases == nil { return nil } life, err := s.store.AppLife(accountID, app) if err != nil || life == nil { return err } tomb, err := release.ReadAppTombstone(ctx, s.cfg.Releases, sqid, app) if err != nil { return err } if tomb != nil { switch { case !life.Trashed(): if err := s.store.TrashAppRowContext(ctx, accountID, app, tomb.DeletedAt); err != nil { return err } log.Printf("console: %s/%s is in the trash in the bucket; marking it trashed here", sqid, app) case life.Restoring: if _, err := s.store.SetRestoringContext(ctx, accountID, app, false); err != nil { return err } log.Printf("console: %s/%s: a restore did not remove the trash marker; it stays in the trash", sqid, app) } return nil } if !life.Trashed() { return nil } keys, err := s.cfg.Releases.List(ctx, release.AppPrefix(sqid, app)) if err != nil { return err } if len(keys) > 0 { if err := s.store.FinishRestoreRow(accountID, app, now); err != nil { return err } log.Printf("console: %s/%s has no trash marker and still has objects; restoring it here", sqid, app) return nil } if !evidence(ctx) { log.Printf("console: %s/%s has no trash marker and no objects, but the apps/ listing holds nothing else either; "+ "leaving the row. Is CARLOS_DEPLOYMENT_BUCKET pointing at the right bucket?", sqid, app) return nil } if err := s.store.PurgeAppRow(accountID, app); err != nil { return err } log.Printf("console: %s/%s is gone from the bucket; releasing the name here", sqid, app) return nil }