| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "fmt" |
| 7 | "math" |
| 8 | "net/http" |
| 9 | "os" |
| 10 | "slices" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/provision" |
| 13 | "github.com/carlosframework/platform/internal/release" |
| 14 | ) |
| 15 | |
| 16 | // refuseSeedMove answers a move of a seed that is not placed (--from) or |
| 17 | // not committed (import) with the deck's sentence (spec §5.8). Both move |
| 18 | // doors call it after their gate and before their transition, so each |
| 19 | // answers the sentence itself; the transitions refuse it again under the |
| 20 | // record CAS. |
| 21 | func (s *Server) refuseSeedMove(w http.ResponseWriter, rec *provision.Record) bool { |
| 22 | if b := rec.Seed.MoveBlock(); b != "" { |
| 23 | jsonError(w, http.StatusConflict, provision.SeedMoveSentence(b)) |
| 24 | return true |
| 25 | } |
| 26 | return false |
| 27 | } |
| 28 | |
| 29 | // placeSeedReadFailed is carlos apps place's answer when the strict read of |
| 30 | // the app's records fails, before anything has moved. The listing after the |
| 31 | // placement is written keeps main's sentence (placeInstancesFailed). |
| 32 | const placeSeedReadFailed = "could not update this app's instances. Try again." |
| 33 | |
| 34 | // placeSeedBlock is carlos apps place's seed rule (preflight M8(a)): a |
| 35 | // place re-homes every record of the app, so it is a move of each. It reads |
| 36 | // the app's records strictly and returns the first record's MoveBlock, "" |
| 37 | // when none blocks. A record it cannot read could be anything, so it is an |
| 38 | // error and the place is refused. A record deleted between the listing and |
| 39 | // its read (the race the place's own walk skips) is read again once. |
| 40 | // |
| 41 | // No cap: the place walks every record of the app, and a cap would refuse |
| 42 | // a large app's place for good. |
| 43 | func placeSeedBlock(ctx context.Context, st release.Store, acct, app string) (string, error) { |
| 44 | ctx, cancel := context.WithTimeout(ctx, instanceStoreTimeout) |
| 45 | defer cancel() |
| 46 | recs, unreadable, err := provision.ListRecordsStrict(ctx, st, acct, app, math.MaxInt) |
| 47 | if errors.Is(err, os.ErrNotExist) { |
| 48 | recs, unreadable, err = provision.ListRecordsStrict(ctx, st, acct, app, math.MaxInt) |
| 49 | } |
| 50 | if err != nil { |
| 51 | return "", err |
| 52 | } |
| 53 | if len(unreadable) > 0 { |
| 54 | hosts := make([]string, 0, len(unreadable)) |
| 55 | for h := range unreadable { |
| 56 | hosts = append(hosts, h) |
| 57 | } |
| 58 | slices.Sort(hosts) |
| 59 | return "", fmt.Errorf("records that do not read: %v", hosts) |
| 60 | } |
| 61 | for _, rec := range recs { |
| 62 | if b := rec.Seed.MoveBlock(); b != "" { |
| 63 | return b, nil |
| 64 | } |
| 65 | } |
| 66 | return "", nil |
| 67 | } |
| 68 | |
| 69 | // seedMoveConflict is the sentence of a write the record's seed refused |
| 70 | // under its CAS (provision.SeedMoveError), "" for any other error. |
| 71 | func seedMoveConflict(err error) string { |
| 72 | var refusal *provision.SeedMoveError |
| 73 | if errors.As(err, &refusal) { |
| 74 | return provision.SeedMoveSentence(refusal.Block) |
| 75 | } |
| 76 | return "" |
| 77 | } |
| 78 | |