package runs import ( "context" "encoding/json" "errors" "os" "sort" "github.com/carlosframework/platform/internal/release" ) // checkRun is what every helper below does first. It refuses an account or // app that release does not accept (ErrBadApp) and an id that is not a run // id (ErrBadID) before it builds a key, so no caller can reach another run's objects, another app's runs, or // a key with a ".." in it, through a crafted value. Nothing is read or written // for a refused call. func checkRun(acct, app, id string) error { if !release.ValidAccount(acct) || !release.ValidApp(app) { return ErrBadApp } if !ValidID(id) { return ErrBadID } return nil } // ReadRequest reads request.json; absent is os.ErrNotExist. func ReadRequest(ctx context.Context, s release.Store, acct, app, id string) (*Request, error) { if err := checkRun(acct, app, id); err != nil { return nil, err } raw, _, err := s.Get(ctx, RequestKey(acct, app, id)) if err != nil { return nil, err } var r Request if err := json.Unmarshal(raw, &r); err != nil { return nil, err } return &r, nil } // ReadResult reads result.json; absent is (nil, nil). func ReadResult(ctx context.Context, s release.Store, acct, app, id string) (*Result, error) { if err := checkRun(acct, app, id); err != nil { return nil, err } raw, _, err := s.Get(ctx, ResultKey(acct, app, id)) if errors.Is(err, os.ErrNotExist) { return nil, nil } if err != nil { return nil, err } var r Result if err := json.Unmarshal(raw, &r); err != nil { return nil, err } return &r, nil } // WriteResult writes result.json create-only: the first result wins, and a // later writer (the sweep's lost result racing the box's) changes nothing. func WriteResult(ctx context.Context, s release.Store, acct, app, id string, r Result) error { if err := checkRun(acct, app, id); err != nil { return err } body, err := json.Marshal(r) if err != nil { return err } if _, err := s.Put(ctx, ResultKey(acct, app, id), body, ""); err != nil && !errors.Is(err, release.ErrCASConflict) { return err } return nil } // ListChunks lists chunk keys after seq, sorted, at most limit (0: all). func ListChunks(ctx context.Context, s release.Store, acct, app, id string, after int64, limit int) ([]string, error) { if err := checkRun(acct, app, id); err != nil { return nil, err } prefix := OutPrefix(acct, app, id) startAfter := prefix if after > 0 { startAfter = ChunkKey(acct, app, id, after, "~") // "~" sorts after both streams of seq } if pl, ok := s.(release.PageLister); ok { return pl.ListPage(ctx, prefix, startAfter, limit) } keys, err := s.List(ctx, prefix) if err != nil { return nil, err } sort.Strings(keys) var out []string for _, k := range keys { if k <= startAfter { continue } out = append(out, k) if limit > 0 && len(out) == limit { break } } return out, nil } // LastChunkSeq is the highest seq written, 0 when none. func LastChunkSeq(ctx context.Context, s release.Store, acct, app, id string) (int64, error) { if err := checkRun(acct, app, id); err != nil { return 0, err } keys, err := ListChunks(ctx, s, acct, app, id, 0, 0) if err != nil { return 0, err } var last int64 for _, k := range keys { if seq, _, ok := ParseChunkKey(k, OutPrefix(acct, app, id)); ok && seq > last { last = seq } } return last, nil } // DeleteRun deletes every object of the run, in an order that keeps a // half-deleted run findable and audited once. The input and output objects // go first; result.json goes only after they are all gone, so a run whose // delete failed part way still answers a follower with how it ended; // request.json, which the orphan sweep ages a run by and which says whose // run it was, goes after the result; the audited marker goes last, so a run // that still has any object still has its marker, and nothing can write the // run-end row a second time. A tier that fails stops the later ones, and the // call can be repeated. func DeleteRun(ctx context.Context, s release.Store, acct, app, id string) error { if err := checkRun(acct, app, id); err != nil { return err } keys, err := s.List(ctx, Prefix(acct, app, id)) if err != nil { return err } result, request, audited := ResultKey(acct, app, id), RequestKey(acct, app, id), AuditedKey(acct, app, id) tiers := [4][]string{} for _, k := range keys { switch k { case audited: tiers[3] = append(tiers[3], k) case request: tiers[2] = append(tiers[2], k) case result: tiers[1] = append(tiers[1], k) default: tiers[0] = append(tiers[0], k) } } for _, tier := range tiers { var errs []error for _, k := range tier { if err := s.Delete(ctx, k); err != nil { errs = append(errs, err) } } if err := errors.Join(errs...); err != nil { return err } } return nil }