carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3// A replica gets its home's key (instance-key plan rulings 26 to 28). A
4// replica (#162) mirrors its home's data, sealed with the home's key, so
5// the only key it may be handed is exactly the home's effective key. The
6// platform never mints one on a replica row (ensureInstanceKeyFor), and
7// every replica launch passes replicaKeyGate before anything runs: the
8// flat seed and spawn (seedChild, spawnChildFrom), the nested run, seed
9// and sidecar roles (prepareAt), the worker (StartWorkerStaged), the
10// legacy co-spawned sidecar (startSidecarChildFrom) and the claim launch
11// sources' selection (SelectClaimLaunch, which has no control socket until
12// O6; the claim's input policy snapshots without the gate, because it
13// launches nothing).
14//
15// - a replica whose env files deliver a key starts only when that key's
16// digest is its entry in the box's verified list (ReplicaHomeKeys, which
17// only the pre-roll check writes, read again at every launch) and, when
18// the key comes from the replica's configuration, the configuration in
19// the bucket renders that same key now (ConfiguredKey). Where the key
20// came from is decided by the same read that composed the env the child
21// gets (instanceEnvLayer, ruling 29), never by reading the files again.
22// A key that differs from either, or that cannot be checked, refuses
23// the launch with a log line. The fresh read catches a configuration
24// change that this box has not synced yet while the home already holds
25// the new key (ruling 27);
26// - a replica whose env files deliver no key starts only with a control
27// socket. There an app on rastrillo's InstanceKey with no key of its
28// own gets ErrNoInstanceKey and refuses (step 3), and an app that never
29// asks for a key runs as before. Without a socket (a seed, a worker, a
30// legacy sidecar, a claim launch before O6, a control bind that
31// failed) the helper would fall to step 4 and mint its own key over
32// the home's data, so the agent refuses the launch itself rather than
33// leave that to the helper.
34//
35// Delivering the home's key to a replica that has none is O8's, for a
36// platform-minted home key only (ruling 28): until then, and always for a
37// home whose key exists only in configuration, a keyless replica gets
38// nothing and is gated as above.
39
40import (
41 "context"
42 "errors"
43 "fmt"
44
45 "github.com/carlosframework/platform/internal/instancekey"
46 "github.com/carlosframework/platform/internal/registry"
47)
48
49// ErrReplicaKey is a replica launch refused by replicaKeyGate.
50var ErrReplicaKey = errors.New("activator: replica launch refused: its instance key is not its home's")
51
52// instanceEnvLayer is instanceEnv returning, from the same read, the layer
53// its CARLOS_INSTANCE_KEY came from (ruling 29). mergeEnv delivers exactly
54// the key instancekey.Effective names, so env[Var] and the layer always
55// describe the same bytes, however the files change afterwards.
56func (b *ExecBackend) instanceEnvLayer(rt registry.Route) (map[string]string, instancekey.Layer, error) {
57 cfgEnv, ownEnv, err := b.instanceEnvParts(rt)
58 if err != nil {
59 return nil, instancekey.LayerNone, err
60 }
61 _, layer := instancekey.Effective(cfgEnv, ownEnv)
62 return mergeEnv(cfgEnv, ownEnv), layer, nil
63}
64
65// sidecarKeyLayer is the layer of the key in a legacy co-spawned sidecar's
66// env, its own hand-staged <host>-sidecar.env (one file, never merged with
67// the instance's layers): LayerOwn when that file holds a key, as the
68// instance's own <host>.env would. Nothing syncs that file from the bucket,
69// so a key there is checked against the home's listed digest with no fresh
70// read, and a keyless one is gated as any launch without a socket is.
71func sidecarKeyLayer(env map[string]string) instancekey.Layer {
72 if env[instancekey.Var] != "" {
73 return instancekey.LayerOwn
74 }
75 return instancekey.LayerNone
76}
77
78// replicaKeyGate is rulings 26, 27 and 29's check for a launch of rt with
79// env (the composed env files), layer (where env's key came from, decided
80// by the same read) and, when haveControl, a bound control socket. It reads
81// no env file. It is nil for every row that is not a replica. Its errors and
82// log lines name the host and the reason, never a key or a digest. The
83// log line is said once an hour per host and reason (logPerFile): a
84// keyless replica's legacy sidecar is refused at every retry of the
85// sidecar sweep, and the line is for the operator, who needs it once. A
86// bucket error is logged as its own argument, apart from the fixed
87// reason, so a reader can see where the text came from.
88func (b *ExecBackend) replicaKeyGate(rt registry.Route, env map[string]string, layer instancekey.Layer, haveControl bool) error {
89 if !rt.Replica {
90 return nil
91 }
92 if instanceKeyHooks.replicaGate != nil {
93 instanceKeyHooks.replicaGate()
94 }
95 refuse := func(why string, cause error) error {
96 if cause != nil {
97 logPerFile("replica-gate\x00"+why, rt.Host, fmt.Sprintf("activator: replica %s not started: %s: %v", rt.Host, why, cause))
98 return fmt.Errorf("%w: %s: %s: %w", ErrReplicaKey, rt.Host, why, cause)
99 }
100 logPerFile("replica-gate\x00"+why, rt.Host, fmt.Sprintf("activator: replica %s not started: %s", rt.Host, why))
101 return fmt.Errorf("%w: %s: %s", ErrReplicaKey, rt.Host, why)
102 }
103 key := env[instancekey.Var]
104 if key == "" {
105 if haveControl {
106 return nil
107 }
108 return refuse("it has no key of its home's and no control socket, so an app on the instance-key helper would mint its own", nil)
109 }
110 var home string
111 var ok bool
112 if b.ReplicaHomeKeys != nil {
113 home, ok = b.ReplicaHomeKeys(rt.Host)
114 }
115 switch {
116 case !ok:
117 return refuse(instancekey.Var+" cannot be checked against its home's key (no entry for it in /etc/carlos/replica-home-keys; O8)", nil)
118 case instancekey.Digest(key) != home:
119 return refuse(instancekey.Var+" is not its home's key", nil)
120 }
121 switch layer {
122 case instancekey.LayerOwn:
123 return nil // the replica's own file: it changes only by hand
124 case instancekey.LayerConfig:
125 default:
126 return refuse("where its "+instancekey.Var+" came from is unknown", nil)
127 }
128 configured := b.configuredKeyFor(rt)
129 if configured == nil {
130 return refuse("its configured "+instancekey.Var+" cannot be checked against the bucket's configuration", nil)
131 }
132 fresh, err := configured(context.Background())
133 switch {
134 case err != nil:
135 return refuse("the bucket's configuration could not be read", err)
136 case fresh != instancekey.Digest(key):
137 return refuse("its configured "+instancekey.Var+" is not the one the bucket's configuration now holds: a key change is in flight (ruling 27)", nil)
138 }
139 return nil
140}
141