carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostclaim
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "math"
11 "os"
12
13 "github.com/carlosframework/platform/internal/release"
14)
15
16var ErrConflict = fmt.Errorf("hostclaim: claim changed since it was read: %w", release.ErrCASConflict)
17
18type Observed struct {
19 Host string
20 Claim *Claim
21 ETag string
22 Absent bool
23}
24
25func decodeStrict(body []byte) (*Claim, error) {
26 dec := json.NewDecoder(bytes.NewReader(body))
27 dec.DisallowUnknownFields()
28 var c Claim
29 if err := dec.Decode(&c); err != nil {
30 return nil, fmt.Errorf("%w: %v", ErrMalformed, err)
31 }
32 if _, err := dec.Token(); err != io.EOF {
33 return nil, fmt.Errorf("%w: trailing data", ErrMalformed)
34 }
35 return &c, nil
36}
37
38func Read(ctx context.Context, s release.Store, host string) (Observed, error) {
39 key, err := Key(host)
40 if err != nil {
41 return Observed{}, err
42 }
43 body, etag, err := s.Get(ctx, key)
44 if errors.Is(err, os.ErrNotExist) {
45 return Observed{Host: host, Absent: true}, nil
46 }
47 if err != nil {
48 return Observed{}, err // C6: an error is never absence
49 }
50 c, err := decodeStrict(body)
51 if err != nil {
52 return Observed{}, err
53 }
54 if c.Host != host {
55 return Observed{}, fmt.Errorf("%w: host mismatch", ErrMalformed)
56 }
57 if err := c.Validate(); err != nil {
58 return Observed{}, err
59 }
60 return Observed{Host: host, Claim: c, ETag: etag}, nil
61}
62
63func Update(ctx context.Context, s release.Store, obs Observed, tr func(*Claim) error) (Observed, error) {
64 key, err := Key(obs.Host)
65 if err != nil {
66 return Observed{}, err
67 }
68 var next *Claim
69 var oldGen uint64
70 if obs.Absent {
71 next = &Claim{V: schemaVersion, Host: obs.Host}
72 } else {
73 if obs.ETag == "" {
74 return Observed{}, fmt.Errorf("%w: non-absent observation without ETag", ErrMalformed)
75 }
76 if obs.Claim == nil || obs.Claim.Host != obs.Host {
77 return Observed{}, fmt.Errorf("%w: observation not bound to %s", ErrMalformed, obs.Host)
78 }
79 raw, err := json.Marshal(obs.Claim)
80 if err != nil {
81 return Observed{}, err
82 }
83 if next, err = decodeStrict(raw); err != nil {
84 return Observed{}, err
85 }
86 oldGen = obs.Claim.Gen
87 if obs.Claim.Origins != nil || originEnabled() {
88 fresh, err := Read(ctx, s, obs.Host)
89 if err != nil {
90 return Observed{}, err
91 }
92 if fresh.Absent || fresh.ETag != obs.ETag || !sameOriginHead(fresh.Claim.Origins, obs.Claim.Origins) {
93 return Observed{}, ErrConflict
94 }
95 }
96 }
97 if err := tr(next); err != nil {
98 return Observed{}, err
99 }
100 if next.Host != obs.Host || next.Gen != oldGen {
101 return Observed{}, fmt.Errorf("%w: a transition may not change host or gen", ErrMalformed)
102 }
103 if oldGen == math.MaxUint64 {
104 return Observed{}, fmt.Errorf("%w: gen overflow", ErrMalformed)
105 }
106 next.Gen = oldGen + 1
107 if err := next.Validate(); err != nil {
108 return Observed{}, err
109 }
110 if err := commitOrigins(ctx, s, obs, next); err != nil {
111 return Observed{}, err
112 }
113 body, err := json.Marshal(next)
114 if err != nil {
115 return Observed{}, err
116 }
117 ifMatch := obs.ETag
118 if obs.Absent {
119 ifMatch = "" // the host's very first claim (C5a)
120 }
121 etag, err := s.Put(ctx, key, body, ifMatch)
122 if errors.Is(err, release.ErrCASConflict) {
123 return Observed{}, ErrConflict
124 }
125 if err != nil {
126 return Observed{}, err
127 }
128 return Observed{Host: obs.Host, Claim: next, ETag: etag}, nil
129}
130