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 "bytes"
5 "context"
6 "encoding/json"
7 "io"
8 "net"
9 "net/http"
10 "path/filepath"
11 "time"
12
13 "github.com/carlosframework/platform/internal/hostbox"
14 "github.com/carlosframework/platform/internal/hostclaim"
15 "github.com/carlosframework/platform/internal/hostguard"
16 "github.com/carlosframework/platform/internal/instancectl"
17)
18
19type coldHandoverRequest struct {
20 Prior hostguard.FenceTarget
21 Next hostbox.ColdSuccessor
22}
23
24func (h ClaimRootHTTP) serveColdHandover(w http.ResponseWriter, r *http.Request, peer StopPeer) {
25 var request coldHandoverRequest
26 decoder := json.NewDecoder(io.LimitReader(r.Body, 32<<10))
27 decoder.DisallowUnknownFields()
28 if decoder.Decode(&request) != nil || decoder.Decode(new(any)) != io.EOF || request.Prior.Box != h.Reconcile.Box || request.Prior.Enrollment != h.Reconcile.Enrollment {
29 w.WriteHeader(http.StatusBadRequest)
30 return
31 }
32 if request.Prior.Session == 0 && request.Prior.Op == "" && request.Prior.Phase == "" {
33 prior, err := h.servingLegacyHandoverTarget(r.Context(), request)
34 if err != nil {
35 w.WriteHeader(http.StatusConflict)
36 return
37 }
38 request.Prior = prior
39 }
40 if request.Prior.Session != 0 && (request.Prior.BootID != peer.BootID || request.Prior.ControllerID != peer.ControllerID) {
41 // ServeHTTP authenticates the live requester independently. Recovery
42 // preserves the predecessor's recorded tuple; only its exact completed
43 // root receipt grants cleanup authority, never the requester's tuple.
44 receipt, err := h.Stop.Coordinator.Guard.FenceReceiptFor(r.Context(), request.Prior)
45 if err != nil || verifyRetirementReceipt(r.Context(), receipt) != nil {
46 w.WriteHeader(http.StatusBadRequest)
47 return
48 }
49 }
50 if h.Launches == nil {
51 w.WriteHeader(http.StatusServiceUnavailable)
52 return
53 }
54 handover := h.coldHandover()
55 var err error
56 if request.Next.Op == "" && request.Next.Phase == "" {
57 serving := h.servingHandover()
58 serving.Validate = func(ctx context.Context, c *hostclaim.Claim, p hostguard.FenceTarget, n hostbox.ColdSuccessor, in hostbox.StopInventory, installation hostguard.RootInstallation) error {
59 fresh, err := h.Stop.Authenticate(ctx, instancectl.RequestConn(r.Context()))
60 if err != nil {
61 return err
62 }
63 if fresh != peer {
64 return ErrClaimStopIdentity
65 }
66 return h.validateServingHandover(ctx, c, p, n, in, installation)
67 }
68 err = serving.Run(r.Context(), request.Prior, request.Next)
69 } else {
70 err = handover.Run(r.Context(), request.Prior, request.Next)
71 }
72 if err != nil {
73 w.WriteHeader(http.StatusConflict)
74 return
75 }
76 w.WriteHeader(http.StatusNoContent)
77}
78
79// coldHandover is root's operation cold handover. Its successor admission
80// (roster binding N5, B1) runs before Run takes any step, for a new
81// handover and for a replay alike: the prior keeps its historical authority
82// (its exact receipt, record-to-record checks), the successor must be
83// stamped with the binding the roster gives this box now.
84func (h ClaimRootHTTP) coldHandover() hostbox.ColdHandover {
85 return hostbox.ColdHandover{OperationsOnly: true, Guard: h.Stop.Coordinator.Guard, Claims: h.Stop.Coordinator.Evidence, Registry: h.Reconcile.Registry, Inventory: h.Reconcile.Inventory, Launches: h.Launches, Roots: h.Roots, JournalDir: filepath.Join(h.Reconcile.Retirement.JournalDir, "cold"), VerifyReceipt: verifyRetirementReceipt, Admit: h.admitColdSuccessor}
86}
87
88// admitColdSuccessor is the fresh successor check (roster binding N5): new
89// work must be allowed (a quarantined box is refused) and the successor must
90// name this box, its enrollment and its binding as the fresh identity reads
91// them. It is checked before any successor state exists.
92func (h ClaimRootHTTP) admitColdSuccessor(ctx context.Context, next hostbox.ColdSuccessor) error {
93 if h.InventoryActor == nil {
94 return hostbox.ErrUnsupported
95 }
96 if h.RequireNewWork != nil {
97 if err := h.RequireNewWork(ctx); err != nil {
98 return err
99 }
100 }
101 actor, err := h.InventoryActor(ctx)
102 if err != nil {
103 return err
104 }
105 if actor.Binding == "" || next.Box != actor.Box || next.Enrollment != actor.Enrollment || next.DirectoryRevision != actor.Binding {
106 return hostbox.ErrChanged
107 }
108 return nil
109}
110
111func coldAdmissionHook(guard hostguard.Guard, identity hostbox.IdentityProvider, socket string) func(context.Context, hostclaim.Observed, hostclaim.Observed) error {
112 return coldHandoverWith(identity, socket, func(ctx context.Context, prior hostclaim.Observed) (hostguard.FenceReceipt, error) {
113 old := prior.Claim.Holder
114 if old.Session == 0 && prior.Claim.Op.Kind == hostclaim.KindServing && prior.Claim.Op.ID == "" && prior.Claim.Op.Phase == "" {
115 actor, err := identity.Verify(ctx)
116 if err != nil {
117 return hostguard.FenceReceipt{}, err
118 }
119 return hostguard.FenceReceipt{Target: hostguard.FenceTarget{Host: prior.Host, Incarnation: old.Incarnation, Stamp: old.Stamp, Box: actor.Box, Enrollment: actor.Enrollment}}, nil
120 }
121 return guard.CompletedColdPredecessor(ctx, prior.Host, hostguard.Captured{Incarnation: old.Incarnation, Stamp: old.Stamp}, old.Session, prior.Claim.Op.ID, prior.Claim.Op.Phase)
122 })
123}
124
125// coldHandoverWith is coldAdmissionHook with the predecessor's completed
126// receipt found by find (extracted unchanged; region HA N6): root's
127// ColdHandover.Run validates the exact prior receipt and the successor.
128func coldHandoverWith(identity hostbox.IdentityProvider, socket string, find func(context.Context, hostclaim.Observed) (hostguard.FenceReceipt, error)) func(context.Context, hostclaim.Observed, hostclaim.Observed) error {
129 return func(ctx context.Context, prior, admitted hostclaim.Observed) error {
130 if prior.Claim == nil || prior.Claim.Holder == nil || admitted.Claim == nil || admitted.Claim.Holder == nil || prior.Host != admitted.Host {
131 return hostbox.ErrChanged
132 }
133 next := admitted.Claim.Holder
134 receipt, err := find(ctx, prior)
135 if err != nil {
136 return err
137 }
138 actor, err := identity.Verify(ctx)
139 if err != nil {
140 return err
141 }
142 request := coldHandoverRequest{Prior: receipt.Target, Next: hostbox.ColdSuccessor{Host: admitted.Host, Incarnation: next.Incarnation, Stamp: next.Stamp, Session: next.Session, Generation: next.Generation, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: actor.Binding, ETag: admitted.ETag, Op: admitted.Claim.Op.ID, Phase: admitted.Claim.Op.Phase}}
143 body, err := json.Marshal(request)
144 if err != nil {
145 return err
146 }
147 transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
148 return (&net.Dialer{}).DialContext(ctx, "unix", socket)
149 }}
150 defer transport.CloseIdleConnections()
151 req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://instance-control/claim/handover", bytes.NewReader(body))
152 if err != nil {
153 return err
154 }
155 response, err := (&http.Client{Transport: transport, Timeout: 2 * time.Minute}).Do(req)
156 if err != nil {
157 return err
158 }
159 defer response.Body.Close()
160 if response.StatusCode != http.StatusNoContent {
161 return hostbox.ErrChanged
162 }
163 return nil
164 }
165}
166