carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package plans
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "os"
9 "time"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// RecordSchema is the limits record's wire version. Fields are only ever
15// added (omitempty), and readers ignore fields they do not know, so an
16// older box reads a newer record.
17const RecordSchema = 1
18
19// Record is one account's effective limits as published to the bucket by
20// the console, its only writer (spec §7). Boxes read it beside the
21// account's manifest, which the fleet read rule already exposes to the
22// account's own boxes (internal/edge/storedoor.go).
23type Record struct {
24 Schema int `json:"schema"`
25 Account string `json:"account"` // sqid; must equal the key's
26 CatalogID string `json:"catalog_id"`
27 Plan string `json:"plan"`
28 Source string `json:"source"`
29 Revision int64 `json:"revision"`
30 Limits Limits `json:"limits"`
31 IssuedAt time.Time `json:"issued_at"`
32}
33
34// LimitsKey is where an account's limits record lives.
35func LimitsKey(sqid string) string { return "accounts/" + sqid + "/limits.json" }
36
37// ParseLimits decodes a record read from sqid's key, refusing an unknown
38// schema or a body that names another account.
39func ParseLimits(b []byte, sqid string) (*Record, error) {
40 var r Record
41 if err := json.Unmarshal(b, &r); err != nil {
42 return nil, fmt.Errorf("plans: %s: %w", LimitsKey(sqid), err)
43 }
44 if r.Schema != RecordSchema {
45 return nil, fmt.Errorf("plans: %s: schema %d, want %d", LimitsKey(sqid), r.Schema, RecordSchema)
46 }
47 if r.Account != sqid {
48 return nil, fmt.Errorf("plans: %s: names account %q", LimitsKey(sqid), r.Account)
49 }
50 return &r, nil
51}
52
53// GetLimits reads sqid's record and its etag. An absent record is nil, "",
54// nil (objstore.GetEntitlement's convention).
55func GetLimits(ctx context.Context, s release.Store, sqid string) (*Record, string, error) {
56 if !release.ValidAccount(sqid) {
57 return nil, "", fmt.Errorf("plans: invalid account %q", sqid)
58 }
59 raw, etag, err := s.Get(ctx, LimitsKey(sqid))
60 if err != nil {
61 if errors.Is(err, os.ErrNotExist) {
62 return nil, "", nil
63 }
64 return nil, "", err
65 }
66 r, err := ParseLimits(raw, sqid)
67 if err != nil {
68 return nil, "", err
69 }
70 return r, etag, nil
71}
72
73// PutLimits CAS-writes r under its account's key: ifMatch "" is
74// create-only, otherwise replace-only. Schema is stamped here, not trusted
75// from the caller.
76func PutLimits(ctx context.Context, s release.Store, r *Record, ifMatch string) (string, error) {
77 if !release.ValidAccount(r.Account) {
78 return "", fmt.Errorf("plans: invalid account %q", r.Account)
79 }
80 r.Schema = RecordSchema
81 body, err := json.MarshalIndent(r, "", " ")
82 if err != nil {
83 return "", err
84 }
85 return s.Put(ctx, LimitsKey(r.Account), append(body, '\n'), ifMatch)
86}
87