| 1 | package 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 | |
| 40 | import ( |
| 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. |
| 50 | var 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. |
| 56 | func (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. |
| 71 | func 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. |
| 88 | func (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 | |