carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net"
8 "net/http"
9 "sync"
10 "time"
11
12 "github.com/carlosframework/platform/internal/hostbox"
13 "github.com/carlosframework/platform/internal/hostclaim"
14 "github.com/carlosframework/platform/internal/instancectl"
15 "github.com/carlosframework/platform/internal/rootstate"
16)
17
18var openClaimServingParkAuthority = func() (rootstate.Store, error) {
19 return rootstate.Open("/var/lib/carlos-claims/authority/serving-parks", true, false)
20}
21
22type servingParkAuthority interface {
23 parkServingDatabase(context.Context, net.Conn, servingArtifactIdentity, servingArtifactIdentity) (servingParkAuthorization, error)
24 parkLegacyServingDatabase(context.Context, net.Conn, servingArtifactIdentity, servingLegacyPredecessor) (servingParkAuthorization, error)
25 completeServingDatabasePark(context.Context, net.Conn, string, servingArtifactIdentity) error
26 retainLegacyNoProcess(context.Context, hostclaim.Observed, StopResult) error
27}
28
29type servingEmptyAuthority interface {
30 EmptyAuthority(context.Context, hostclaim.Observed, ClaimLaunchInputs) error
31}
32
33// Closing cancels and joins every request and stop-evidence retention before
34// releasing private state. A timed-out join leaves the store open for retry.
35type servingParkRuntime struct {
36 authority servingParkAuthority
37 state rootstate.Store
38 ctx context.Context
39 cancel context.CancelFunc
40 mu sync.Mutex
41 active int
42 recoveryMu sync.Mutex
43 recoveryAfter string
44 lastRecoveryLog time.Time
45 closing, closed, started bool
46 idle chan struct{}
47}
48
49func withServingParkRoot(h ClaimRootHTTP, empty servingEmptyAuthority) (ClaimRootHTTP, error) {
50 state, err := openClaimServingParkAuthority()
51 if err != nil {
52 return ClaimRootHTTP{}, err
53 }
54 runtime, err := newServingParkRuntime(h, state, empty)
55 if err != nil {
56 state.Close()
57 return ClaimRootHTTP{}, err
58 }
59 h.servingParks = runtime
60 h.Stop.Coordinator.RetainNoProcess = runtime.retainNoProcess
61 return h, nil
62}
63
64func newServingParkRuntime(h ClaimRootHTTP, state rootstate.Store, empty servingEmptyAuthority) (*servingParkRuntime, error) {
65 authority, err := newServingParkAuthority(h, state, empty)
66 if err != nil {
67 return nil, err
68 }
69 ctx, cancel := context.WithCancel(context.Background())
70 return &servingParkRuntime{authority: authority, state: state, ctx: ctx, cancel: cancel, idle: make(chan struct{})}, nil
71}
72
73func (p *servingParkRuntime) begin(ctx context.Context) (context.Context, func(), error) {
74 if p == nil || ctx == nil {
75 return nil, nil, hostbox.ErrUnsupported
76 }
77 p.mu.Lock()
78 defer p.mu.Unlock()
79 if p.closing {
80 return nil, nil, hostbox.ErrUnsupported
81 }
82 p.active++
83 call, cancel := context.WithCancel(ctx)
84 stop := context.AfterFunc(p.ctx, cancel)
85 return call, func() {
86 stop()
87 cancel()
88 p.mu.Lock()
89 defer p.mu.Unlock()
90 p.active--
91 if p.closing && p.active == 0 {
92 close(p.idle)
93 }
94 }, nil
95}
96
97func (p *servingParkRuntime) Close(ctx context.Context) error {
98 if p == nil {
99 return nil
100 }
101 p.mu.Lock()
102 if !p.closing {
103 p.closing = true
104 p.cancel()
105 if p.active == 0 {
106 close(p.idle)
107 }
108 }
109 p.mu.Unlock()
110 select {
111 case <-p.idle:
112 case <-ctx.Done():
113 select {
114 case <-p.idle:
115 default:
116 return ctx.Err()
117 }
118 }
119 p.mu.Lock()
120 defer p.mu.Unlock()
121 if p.closed {
122 return nil
123 }
124 p.closed = true
125 return p.state.Close()
126}
127
128func (p *servingParkRuntime) retainNoProcess(ctx context.Context, o hostclaim.Observed, result StopResult) error {
129 call, done, err := p.begin(ctx)
130 if err != nil {
131 return err
132 }
133 defer done()
134 return p.authority.retainLegacyNoProcess(call, o, result)
135}
136
137func (h ClaimRootHTTP) serveServingPark(w http.ResponseWriter, r *http.Request, q hostbox.JournalRequest) {
138 out := hostbox.JournalResponse{V: 1, ID: q.ID}
139 ctx, done, err := h.servingParks.begin(r.Context())
140 if err != nil {
141 w.WriteHeader(http.StatusServiceUnavailable)
142 return
143 }
144 defer done()
145 conn := instancectl.RequestConn(ctx)
146 if conn == nil || h.RequireNewWork == nil || h.InventoryActor == nil || h.Journals == nil || q.ServingPark == nil {
147 w.WriteHeader(http.StatusForbidden)
148 return
149 }
150 // This gate also covers receipt-only replay. The outer HTTP handler has
151 // authenticated the live edge main process; effects authenticate it again
152 // after acquiring root's namespace and host locks.
153 if err = h.RequireNewWork(ctx); err != nil {
154 w.WriteHeader(http.StatusForbidden)
155 return
156 }
157 actor, err := h.InventoryActor(ctx)
158 if err != nil || !actor.Eligible || actor.Quarantined || actor.Box == "" || actor.Enrollment == "" || actor.Revision == 0 || actor.Binding == "" || q.ServingPark.Successor.Box != actor.Box || q.ServingPark.Successor.Region != actor.Region {
159 w.WriteHeader(http.StatusForbidden)
160 return
161 }
162 out, err = h.Journals.ExecuteServingPark(ctx, actor.Box+"/"+actor.Enrollment, q, func(ctx context.Context) (hostbox.ServingParkResult, error) {
163 p, a := *q.ServingPark, h.servingParks.authority
164 switch q.Op {
165 case "serving-park":
166 return a.parkServingDatabase(ctx, conn, p.Successor, p.Predecessor)
167 case "serving-park-legacy":
168 return a.parkLegacyServingDatabase(ctx, conn, p.Successor, p.Legacy)
169 case "serving-park-complete":
170 err := a.completeServingDatabasePark(ctx, conn, p.Retirement, p.Successor)
171 return hostbox.ServingParkResult{ID: p.Retirement, Successor: p.Successor}, err
172 default:
173 return hostbox.ServingParkResult{}, hostbox.ErrUnsupported
174 }
175 })
176 if err != nil {
177 out.ServingPark = nil
178 switch {
179 case errors.Is(err, hostbox.ErrJournalUncertain):
180 out.Error = "uncertain"
181 case errors.Is(err, hostbox.ErrChanged), errors.Is(err, hostbox.ErrTypedJournal):
182 out.Error = "refused"
183 case errors.Is(err, hostbox.ErrUnsupported):
184 out.Error = "unsupported"
185 default:
186 out.Error = "uncertain"
187 }
188 }
189 w.Header().Set("Content-Type", "application/json")
190 _ = json.NewEncoder(w).Encode(out)
191}
192