carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package appconfig
2
3import (
4 "context"
5 "crypto/ecdh"
6 "crypto/sha256"
7 "encoding/hex"
8 "errors"
9 "fmt"
10 "os"
11 "path/filepath"
12 "time"
13
14 "github.com/carlosframework/platform/internal/hostwrite"
15 "github.com/carlosframework/platform/internal/registry"
16 "github.com/carlosframework/platform/internal/release"
17)
18
19// ErrNotHeld is RenderHeld refusing a route it does not own: a row SyncExec
20// converges (status "" or "ready"), a row held by another operation, a row
21// of a shape no move takes, or a call without the op and box the ack is
22// bound to.
23var ErrNotHeld = errors.New("config: not a held row of this move")
24
25// ErrHeldClaimWired is RenderHeld refusing to write while CARLOS_HOST_CLAIM
26// is on (ruling F10): a held row's write has no claim selection until Plan
27// 2 adds one under the landing op, and an unguarded write is not added.
28var ErrHeldClaimWired = errors.New("config: held rows are not rendered while CARLOS_HOST_CLAIM is on")
29
30// HeldRender is what RenderHeld rendered: the file, the sha256 of its
31// bytes, and the versions of the objects merged into it. BundleETags has
32// the keys "default" and "environment".
33type HeldRender struct {
34 Path string
35 SHA256 string
36 BundleETags map[string]string
37 OverlayETag string
38}
39
40// RenderHeld renders the config file of a held row: one that a move holds
41// on this box (status not "" or "ready", hold_op empty or op), which
42// SyncExec therefore skips (execRouteBase), so the two never write one file.
43// It merges through LoadMergedHostVersions, writes through the same writer
44// SyncExec uses (renderExecConfig) when the bytes differ, and writes a
45// move-bound ack through WriteConfigAck on every call, changed or not: the
46// ack names op, the file's sha256 and the merged objects' etags, which T3
47// compares (moves design §9.1).
48//
49// It writes through legacyHostWork, so it refuses outright while
50// CARLOS_HOST_CLAIM is on (ErrHeldClaimWired) and writes nothing.
51func RenderHeld(ctx context.Context, s release.Store, priv *ecdh.PrivateKey, dataDir string, rt registry.Route, op string, box string) (HeldRender, error) {
52 if hostwrite.Enabled(os.Getenv) {
53 return HeldRender{}, ErrHeldClaimWired
54 }
55 return renderHeld(ctx, s, priv, dataDir, rt, op, box, func(HeldRender) (hostwrite.Work, error) { return legacyHostWork{}, nil }, nil)
56}
57
58// RenderHeldBefore is RenderHeld with before called once the rendered bytes
59// are known and before anything is written: before receives the hex sha256
60// of exactly the bytes the render then places (or finds already in place),
61// so a caller can make its ownership of the file durable first (moves Plan 5
62// Task 9a, carry (c)). An error from before ends the render: nothing is
63// written and no ack is sent. The write itself is RenderHeld's, unchanged.
64func RenderHeldBefore(ctx context.Context, s release.Store, priv *ecdh.PrivateKey, dataDir string, rt registry.Route, op string, box string, before func(sha256hex string) error) (HeldRender, error) {
65 if before == nil {
66 return HeldRender{}, hostwrite.ErrUnavailable
67 }
68 if hostwrite.Enabled(os.Getenv) {
69 return HeldRender{}, ErrHeldClaimWired
70 }
71 return renderHeld(ctx, s, priv, dataDir, rt, op, box, func(HeldRender) (hostwrite.Work, error) { return legacyHostWork{}, nil }, before)
72}
73
74// RenderHeldUnder is RenderHeld for a held row under the host claim (moves
75// Phase 1 Plan 3 Task 5, ruling H13): the same ownership check, merge,
76// renderer, writer and move-bound ack, with the file written through the
77// claim selection work returns instead of legacyHostWork. work is called
78// once the merged versions are known, with BundleETags and OverlayETag set
79// (the rest of the HeldRender empty), so its caller can select a held-config
80// work bound to exactly the etags this render writes; nothing is written
81// before it returns. A nil work, or a work that is nil, is refused. It does
82// not consult CARLOS_HOST_CLAIM: the selection is the authority.
83func RenderHeldUnder(ctx context.Context, s release.Store, priv *ecdh.PrivateKey, dataDir string, rt registry.Route, op string, box string, work func(HeldRender) (hostwrite.Work, error)) (HeldRender, error) {
84 if work == nil {
85 return HeldRender{}, hostwrite.ErrUnavailable
86 }
87 return renderHeld(ctx, s, priv, dataDir, rt, op, box, work, nil)
88}
89
90// RenderHeldUnderBefore is RenderHeldUnder with RenderHeldBefore's before
91// (moves after claim Plan 12, Task 7; platform-1e's answer to Q2.2): the
92// held render the landing's configured step runs under the claim, which
93// keeps carry (c). The ownership checks, the merge, the renderer, the
94// writer and the move-bound ack are RenderHeld's. work is called once the
95// merged versions are known, as for RenderHeldUnder; before is then called
96// with the hex sha256 of exactly the bytes the render places (or finds in
97// place), and only after it returns is anything written, through the
98// selection work returned. An error from before ends the render: nothing is
99// written, the selection is never entered and no ack is sent. A nil work or
100// a nil before is refused before anything is read. Like RenderHeldUnder it
101// does not consult CARLOS_HOST_CLAIM: the selection is the authority.
102func RenderHeldUnderBefore(ctx context.Context, s release.Store, priv *ecdh.PrivateKey, dataDir string, rt registry.Route, op string, box string, work func(HeldRender) (hostwrite.Work, error), before func(sha256hex string) error) (HeldRender, error) {
103 if work == nil || before == nil {
104 return HeldRender{}, hostwrite.ErrUnavailable
105 }
106 return renderHeld(ctx, s, priv, dataDir, rt, op, box, work, before)
107}
108
109func renderHeld(ctx context.Context, s release.Store, priv *ecdh.PrivateKey, dataDir string, rt registry.Route, op string, box string, work func(HeldRender) (hostwrite.Work, error), before func(string) error) (HeldRender, error) {
110 if err := heldRenderable(rt, op, box); err != nil {
111 return HeldRender{}, err
112 }
113 acct := release.RouteAccount(rt)
114 m, v, err := LoadMergedHostVersions(ctx, s, acct, rt.App, rt.Environment, rt.Host)
115 if err != nil {
116 return HeldRender{}, err
117 }
118 w, err := work(HeldRender{BundleETags: map[string]string{"default": v.Default, "environment": v.Environment}, OverlayETag: v.Overlay})
119 if err != nil {
120 return HeldRender{}, err
121 }
122 if w == nil {
123 return HeldRender{}, hostwrite.ErrUnavailable
124 }
125 r, err := renderExecConfigBefore(ctx, w, rt, m, priv, dataDir, "", v.Overlay, before, false)
126 if err != nil {
127 return HeldRender{}, err
128 }
129 sum := sha256.Sum256(r.rendered)
130 out := HeldRender{
131 Path: r.path,
132 SHA256: hex.EncodeToString(sum[:]),
133 BundleETags: map[string]string{"default": v.Default, "environment": v.Environment},
134 OverlayETag: v.Overlay,
135 }
136 a := ConfigAck{Schema: ConfigAckSchema, Box: box, Host: rt.Host, Shape: shapeOf(rt), At: time.Now(),
137 InstanceConfigETag: v.Overlay, MoveOp: op, FileSHA256: out.SHA256,
138 BundleETags: map[string]string{"default": v.Default, "environment": v.Environment}}
139 if suffixes, err := relaySuffixesFromRelayVar(m, priv, routeBinding(rt)); err != nil {
140 fmt.Printf(" %s: held config rendered, but CARLOS_RELAY could not be read for the config-ack: %v\n", rt.Host, err)
141 } else {
142 a.RelaySuffixes = suffixes
143 }
144 if err := WriteConfigAck(ctx, s, a); err != nil {
145 return out, fmt.Errorf("%s: held config ack: %w", rt.Host, err)
146 }
147 return out, nil
148}
149
150// heldRenderable is RenderHeld's admission: the exec-instance shape a move
151// takes (execSyncable without its status clause), a status SyncExec skips,
152// no hold by another operation, a bare host name, and an op and box to bind
153// the ack to.
154func heldRenderable(rt registry.Route, op, box string) error {
155 switch {
156 case op == "" || box == "":
157 return fmt.Errorf("%w: a held render needs a move op and a box", ErrNotHeld)
158 case rt.Host == "" || rt.Host == "." || rt.Host == ".." || rt.Host != filepath.Base(rt.Host):
159 return fmt.Errorf("%w: %q is not a bare host name", ErrNotHeld, rt.Host)
160 case rt.Status == "" || rt.Status == "ready":
161 return fmt.Errorf("%w: %s is %q, which config sync owns", ErrNotHeld, rt.Host, rt.Status)
162 case rt.HoldOp != "" && rt.HoldOp != op:
163 return fmt.Errorf("%w: %s is held by %s", ErrNotHeld, rt.Host, rt.HoldOp)
164 case rt.Kind != "instance" || rt.App == "" || rt.Platform || rt.Backing == registry.BackingUnit || rt.Socket == "":
165 return fmt.Errorf("%w: %s is not an exec-backed instance", ErrNotHeld, rt.Host)
166 }
167 return nil
168}
169