carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "crypto/ed25519"
6 "encoding/json"
7 "io"
8 "time"
9
10 "github.com/carlosframework/platform/internal/hostclaim"
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15type effectPermitKey struct{}
16type effectPermit struct{ Resource string }
17
18// conditionalRecordStore enforces D7's record-shape protocol beneath the
19// mutation boundary: provisioning records are created only inside their own
20// issued record effect; every other write replaces an existing exact ETag
21// and preserves identity. Authority (which app, which permit) is the
22// boundary's job, not this layer's. Named private fields; no embedding.
23type conditionalRecordStore struct {
24 homeRegion string
25 raw release.Store
26 pub ed25519.PublicKey
27}
28
29var (
30 _ release.Store = conditionalRecordStore{}
31 _ release.ConditionalDeleter = conditionalRecordStore{}
32 _ release.Opener = conditionalRecordStore{}
33 _ release.ModLister = conditionalRecordStore{}
34 _ release.SizeLister = conditionalRecordStore{}
35 _ release.PageLister = conditionalRecordStore{}
36 _ release.ConditionalGetter = conditionalRecordStore{}
37 _ release.ETagLister = conditionalRecordStore{}
38 _ release.BucketListDirs = conditionalRecordStore{}
39)
40
41func NewConditionalRecordStore(raw release.Store, pub ed25519.PublicKey, homeRegion ...string) (release.Store, error) {
42 if raw == nil || len(pub) != ed25519.PublicKeySize {
43 return nil, ErrContract
44 }
45 home, err := consoleHomeRegion(homeRegion)
46 if err != nil {
47 return nil, err
48 }
49 return conditionalRecordStore{raw: raw, pub: pub, homeRegion: home}, nil
50}
51
52func provisioningKey(key string) bool {
53 return provision.IsRecordKey(key)
54}
55
56func (s conditionalRecordStore) Get(ctx context.Context, key string) ([]byte, string, error) {
57 return s.raw.Get(ctx, key)
58}
59func (s conditionalRecordStore) List(ctx context.Context, prefix string) ([]string, error) {
60 return s.raw.List(ctx, prefix)
61}
62func (s conditionalRecordStore) ListDirs(ctx context.Context, prefix string) ([]string, error) {
63 return s.raw.ListDirs(ctx, prefix)
64}
65func (s conditionalRecordStore) BucketShapedListDirs() bool { return release.HasBucketListDirs(s.raw) }
66func (s conditionalRecordStore) Open(ctx context.Context, key string) (io.ReadCloser, string, error) {
67 return release.OpenObject(ctx, s.raw, key)
68}
69func (s conditionalRecordStore) ListMod(ctx context.Context, prefix string) ([]release.KeyMod, error) {
70 if l, ok := s.raw.(release.ModLister); ok {
71 return l.ListMod(ctx, prefix)
72 }
73 return nil, ErrContract
74}
75func (s conditionalRecordStore) ListSizes(ctx context.Context, prefix string) ([]release.KeySize, error) {
76 if l, ok := s.raw.(release.SizeLister); ok {
77 return l.ListSizes(ctx, prefix)
78 }
79 return nil, ErrContract
80}
81func (s conditionalRecordStore) ListPage(ctx context.Context, prefix, startAfter string, limit int) ([]string, error) {
82 if l, ok := s.raw.(release.PageLister); ok {
83 return l.ListPage(ctx, prefix, startAfter, limit)
84 }
85 return nil, ErrContract
86}
87
88func (s conditionalRecordStore) GetIfChanged(ctx context.Context, key, etag string) ([]byte, string, bool, error) {
89 if g, ok := s.raw.(release.ConditionalGetter); ok {
90 return g.GetIfChanged(ctx, key, etag)
91 }
92 return nil, "", false, ErrContract
93}
94func (s conditionalRecordStore) ListETags(ctx context.Context, prefix string) ([]release.KeyETag, error) {
95 if l, ok := s.raw.(release.ETagLister); ok {
96 return l.ListETags(ctx, prefix)
97 }
98 return nil, ErrContract
99}
100
101func (s conditionalRecordStore) Put(ctx context.Context, key string, body []byte, etag string) (string, error) {
102 if !provisioningKey(key) {
103 return s.raw.Put(ctx, key, body, etag)
104 }
105 if err := CheckCreateFreeze(ctx, s.raw, s.pub); err != nil {
106 return "", err
107 }
108 // Rev 3 operation-bound lifecycle record writer (lifecycle_record.go).
109 if permit, ok := ctx.Value(lifecycleRecordKey{}).(lifecycleRecordPermit); ok {
110 return s.putLifecycle(ctx, key, body, etag, permit)
111 }
112 if permit, ok := ctx.Value(effectPermitKey{}).(effectPermit); ok && (permit.Resource == key || permit.Resource == "control-object:"+key) {
113 return putSequencedRecord(ctx, s.raw, key, body, etag)
114 }
115 // Not-found is never turned into create outside the record effect.
116 if etag == "" {
117 return "", ErrContract
118 }
119 current, err := ReadRecord(ctx, s.raw, key)
120 if err != nil {
121 return "", err
122 }
123 if current.ETag != etag {
124 return "", release.ErrCASConflict
125 }
126 // Move journal fields change only through the lifecycle writer above.
127 if !sameMoveJournal(current.Body, body) {
128 return "", ErrContract
129 }
130 var next struct {
131 Account string `json:"account"`
132 App string `json:"app"`
133 Host string `json:"host"`
134 Name string `json:"name"`
135 Acquisition string `json:"acq"`
136 Region string `json:"region"`
137 CreatedAt time.Time `json:"created_at"`
138 }
139 if json.Unmarshal(body, &next) != nil {
140 return "", ErrIdentity
141 }
142 n := current.Holder
143 if next.Account != n.Account || next.App != n.App || next.Host != n.Host || next.Name != n.Name || next.Acquisition != n.Acquisition || next.Region != n.RecordRegion || !next.CreatedAt.Equal(n.CreatedAt) {
144 return "", ErrIdentity
145 }
146 obs, err := hostclaim.Read(ctx, s.raw, n.Host)
147 if err != nil {
148 return "", err
149 }
150 if obs.Absent || obs.Claim.Op.Kind != hostclaim.KindServing || !matches(current, obs.Claim.Holder) {
151 return "", ErrPending
152 }
153 return putSequencedRecord(ctx, s.raw, key, body, etag)
154}
155
156func (s conditionalRecordStore) Delete(ctx context.Context, key string) error {
157 if provisioningKey(key) {
158 return ErrContract
159 }
160 return s.raw.Delete(ctx, key)
161}
162
163func (s conditionalRecordStore) DeleteIfMatch(ctx context.Context, key, etag string) error {
164 return release.DeleteIfMatch(ctx, s.raw, key, etag)
165}
166