package plans import ( "context" "encoding/json" "errors" "fmt" "os" "time" "github.com/carlosframework/platform/internal/release" ) // RecordSchema is the limits record's wire version. Fields are only ever // added (omitempty), and readers ignore fields they do not know, so an // older box reads a newer record. const RecordSchema = 1 // Record is one account's effective limits as published to the bucket by // the console, its only writer (spec ยง7). Boxes read it beside the // account's manifest, which the fleet read rule already exposes to the // account's own boxes (internal/edge/storedoor.go). type Record struct { Schema int `json:"schema"` Account string `json:"account"` // sqid; must equal the key's CatalogID string `json:"catalog_id"` Plan string `json:"plan"` Source string `json:"source"` Revision int64 `json:"revision"` Limits Limits `json:"limits"` IssuedAt time.Time `json:"issued_at"` } // LimitsKey is where an account's limits record lives. func LimitsKey(sqid string) string { return "accounts/" + sqid + "/limits.json" } // ParseLimits decodes a record read from sqid's key, refusing an unknown // schema or a body that names another account. func ParseLimits(b []byte, sqid string) (*Record, error) { var r Record if err := json.Unmarshal(b, &r); err != nil { return nil, fmt.Errorf("plans: %s: %w", LimitsKey(sqid), err) } if r.Schema != RecordSchema { return nil, fmt.Errorf("plans: %s: schema %d, want %d", LimitsKey(sqid), r.Schema, RecordSchema) } if r.Account != sqid { return nil, fmt.Errorf("plans: %s: names account %q", LimitsKey(sqid), r.Account) } return &r, nil } // GetLimits reads sqid's record and its etag. An absent record is nil, "", // nil (objstore.GetEntitlement's convention). func GetLimits(ctx context.Context, s release.Store, sqid string) (*Record, string, error) { if !release.ValidAccount(sqid) { return nil, "", fmt.Errorf("plans: invalid account %q", sqid) } raw, etag, err := s.Get(ctx, LimitsKey(sqid)) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil, "", nil } return nil, "", err } r, err := ParseLimits(raw, sqid) if err != nil { return nil, "", err } return r, etag, nil } // PutLimits CAS-writes r under its account's key: ifMatch "" is // create-only, otherwise replace-only. Schema is stamped here, not trusted // from the caller. func PutLimits(ctx context.Context, s release.Store, r *Record, ifMatch string) (string, error) { if !release.ValidAccount(r.Account) { return "", fmt.Errorf("plans: invalid account %q", r.Account) } r.Schema = RecordSchema body, err := json.MarshalIndent(r, "", " ") if err != nil { return "", err } return s.Put(ctx, LimitsKey(r.Account), append(body, '\n'), ifMatch) }