Skip to main content

shep_core/protocol/request/
verbs.rs

1//! [`Request`], one variant per verb, and the selector a verb names a sheep with.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::config::{AppConfig, DeclaredApp, DogTable, ResetDepth};
8
9use super::{DogSectionToml, DogSource, EnvValue, SelectorSpec, Smit};
10
11// Named by intra-doc links and by nothing rustc compiles, so the
12// import is behind `cfg(doc)` rather than flagged unused.
13#[cfg(doc)]
14use super::{Response, RpcErrorCode, SheepApplied, SheepDrift};
15
16/// One RPC request
17// wire format: changing existing variants is a breaking change
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19#[serde(tag = "kind", rename_all = "snake_case")]
20#[non_exhaustive]
21pub enum Request {
22    /// Liveness check
23    Ping,
24    /// Full flock listing
25    ListFlock,
26    /// What the machine the flock runs on is doing
27    ///
28    /// Answers [`Response::HostUsage`]. Its own request rather than a field
29    /// on [`Response::Flock`]: that variant serializes as an array, and
30    /// giving it a second field would retype it into an object that no
31    /// shipped client can read. A daemon that has never heard of this one
32    /// decodes [`Self::Unrecognized`] and refuses by name, which costs the
33    /// caller the host block and nothing else.
34    HostUsage,
35    /// Detailed info for matching sheep
36    Describe {
37        /// Which sheep
38        selector: SelectorSpec,
39    },
40    /// Register + start apps
41    Start {
42        /// App configs. The daemon must re-normalize them, since peer input
43        /// is untrusted; failures return [`RpcErrorCode::InvalidConfig`]
44        apps: Vec<AppConfig>,
45    },
46    /// Register apps as flock members without starting any of them
47    ///
48    /// Each app lands `Stopped` and holds no pid; `shep add` is the verb.
49    ///
50    /// Idempotent by name: an app the flock already has is answered as it
51    /// stands, running or not, and nothing about it changes.
52    /// [`Self::ApplyConfig`] merges a template into one the flock already
53    /// has, and `shep add` sends both.
54    ///
55    /// Answers [`Response::Added`].
56    Add {
57        /// App configs, carried exactly as [`Self::Start`] carries them. The
58        /// daemon must re-normalize them, since peer input is untrusted;
59        /// failures return [`RpcErrorCode::InvalidConfig`]
60        apps: Vec<AppConfig>,
61    },
62    /// Ask which of `apps` name a sheep the flock already has under a
63    /// different config
64    ///
65    /// Read-only. [`Self::Start`] on an already-registered name adds
66    /// instances rather than reconciling config.
67    ///
68    /// Answers [`Response::Drifted`] with one [`SheepDrift`] per app that is
69    /// both registered and different. An app the flock does not have is
70    /// absent from the answer, not reported as unchanged.
71    ConfigDrift {
72        /// The configs to compare against, exactly as [`Self::Start`] would
73        /// carry them. The daemon must re-normalize them: peer input is
74        /// untrusted, and an unnormalized config would report every default
75        /// it has not spelled out as a difference. Failures return
76        /// [`RpcErrorCode::InvalidConfig`].
77        apps: Vec<AppConfig>,
78    },
79    /// Merge each declared app into the sheep of the same name, applying
80    /// what can be applied and parking the rest for that sheep's next spawn
81    ///
82    /// Nothing is registered, nothing is pruned and nothing running is
83    /// killed: an app the flock does not have is refused by name, and a
84    /// field the running child was spawned from waits for a `shep reload`.
85    /// Additive by default; `reset` widens it.
86    ///
87    /// Answers [`Response::Applied`] with one [`SheepApplied`] per entry in
88    /// `apps`, in the order given, found or not and changed or not. One
89    /// app's refusal rides in [`SheepApplied::refused`] and does not cost
90    /// the rest of the file its load.
91    ApplyConfig {
92        /// The apps to merge in, each carrying the keys its document
93        /// literally wrote. The daemon must re-normalize the merge result,
94        /// since peer input is untrusted, and refuses the whole request with
95        /// [`RpcErrorCode::InvalidConfig`] when two entries share a name:
96        /// the second would be merged against a store the first has not
97        /// written yet.
98        apps: Vec<DeclaredApp>,
99        /// How much of what the operator has set since a template last
100        /// loaded this request may overwrite. Default [`ResetDepth::None`],
101        /// which overwrites nothing.
102        ///
103        /// Spelled `none`/`file`/`env`/`policy` on the wire.
104        reset: ResetDepth,
105    },
106    /// One sheep's effective config, for a pane that is about to edit it.
107    ///
108    /// `env` comes back emptied and its key names ride separately, so a
109    /// value never crosses the wire. Read-only: nothing about the sheep
110    /// changes.
111    ///
112    /// Answers [`Response::SheepConfig`], or
113    /// [`RpcErrorCode::NotFound`] when no sheep has that name.
114    SheepConfig {
115        /// The sheep's name, not a selector: a pane edits one sheep, for
116        /// the reason [`Self::Scale`] states at length.
117        name: String,
118    },
119    /// Sets, replaces, or with `None` removes one env key on one sheep,
120    /// recorded as an operator override. Never reads it back.
121    ///
122    /// Its own request rather than a [`Self::ApplyConfig`] depth, because
123    /// no depth does this: `ResetDepth::None` appends only, `File` and
124    /// `Policy` leave env alone, and `Env`/`All` replace the whole map with
125    /// the template's. A pane cannot send the whole map, since it is never
126    /// told the values it would have to send back.
127    ///
128    /// The running child holds the env it was spawned from, so the change
129    /// parks for the next spawn exactly as `ApplyConfig` parks a
130    /// respawn-only field, and `shep reload`/`shep restart` promote it.
131    ///
132    /// Answers [`Response::SheepEnvSet`], or
133    /// [`RpcErrorCode::NotFound`] when no sheep has that name.
134    SetSheepEnv {
135        /// The sheep's name, not a selector, for [`Self::SheepConfig`]'s
136        /// reason.
137        name: String,
138        /// The env key.
139        key: String,
140        /// The value, or `None` to remove the key.
141        ///
142        /// [`EnvValue`], not a bare `String`, for the reason that type's
143        /// own doc gives: this is the most secret-dense field on the wire
144        /// and a derived `Debug` on [`Request`] would print it (IR-41).
145        value: Option<EnvValue>,
146    },
147    /// Sets several env keys on one sheep in a single write.
148    ///
149    /// [`Self::SetSheepEnv`]'s doc says a request taking a map would need
150    /// per-key reporting back "for no caller that wants it". `shep import
151    /// env` is that caller: it writes twenty keys at once and has to refuse
152    /// the whole set rather than leave eleven of them applied.
153    ///
154    /// The daemon applies this as one read-modify-write of the override
155    /// store, so every key lands or none does. No removal arm: a pane
156    /// deletes rows one at a time through [`Self::SetSheepEnv`], and an
157    /// import never removes anything.
158    ///
159    /// A key already holding a different value is a collision. Without
160    /// `force` any collision refuses the whole request and writes nothing;
161    /// with it, the collisions are overwritten and named in the reply as
162    /// well as counted in `set`. A key already holding the same value is
163    /// `unchanged`, never a collision, so re-running an unchanged import
164    /// needs no flag.
165    ///
166    /// `dry_run` computes the three lists and writes nothing.
167    ///
168    /// Parks for the next spawn, exactly as [`Self::SetSheepEnv`] does.
169    ///
170    /// Answers [`Response::SheepEnvBatch`], or
171    /// [`RpcErrorCode::NotFound`] when no sheep has that name.
172    SetSheepEnvBatch {
173        /// The sheep's name.
174        name: String,
175        /// The keys and their values.
176        ///
177        /// [`EnvValue`], not `String`, for [`Self::SetSheepEnv`]'s reason:
178        /// `Request` derives `Debug` and this map is the densest run of
179        /// secrets on the wire (IR-41).
180        entries: BTreeMap<String, EnvValue>,
181        /// Overwrite colliding keys instead of refusing.
182        force: bool,
183        /// Report what would happen and write nothing.
184        dry_run: bool,
185    },
186    /// Sets one config field on one sheep, recorded as an operator
187    /// override.
188    ///
189    /// [`Self::SetSheepEnv`]'s twin for everything that is not `env`, and
190    /// it exists for the reason that one does rather than by symmetry.
191    /// [`Self::ApplyConfig`] can move a single field (one [`DeclaredApp`]
192    /// declaring one key, at [`ResetDepth::File`]), but it moves it as a
193    /// template and spends the operator's override for it. That reasoning
194    /// does not hold here: a pane's value is the operator's, and the sheep
195    /// still differs from its file. Routed through `ApplyConfig`, the `*`
196    /// marker would never appear for that edit.
197    ///
198    /// One field, not a map: a pane edits one row at a time, and a request
199    /// that took several would need [`Response::Applied`]'s per-field
200    /// reporting back again for no caller that wants it.
201    ///
202    /// `env` is refused here and goes through [`Self::SetSheepEnv`]. So are
203    /// `name` and `instances`, which are
204    /// [`ApplyGroup::Structural`](crate::config::ApplyGroup::Structural):
205    /// identity and flock shape rather than runtime knobs, and the count
206    /// moves through [`Self::Scale`].
207    ///
208    /// The four-way apply classification governs exactly as it does for a
209    /// load. A `Live` field is in force at the daemon's next decision, a
210    /// `NextSpawn` field reaches the stored spec, and a `NeedsRespawn`
211    /// field parks for `shep reload` to promote.
212    ///
213    /// Answers [`Response::SheepFieldSet`], or
214    /// [`RpcErrorCode::NotFound`] when no sheep has that name.
215    SetSheepField {
216        /// The sheep's name, not a selector, for [`Self::SheepConfig`]'s
217        /// reason.
218        name: String,
219        /// The [`AppConfig`] field to set. A key that type has no such
220        /// field is refused with [`RpcErrorCode::InvalidConfig`] rather
221        /// than ignored.
222        key: String,
223        /// The new value, in the shape that field serializes as. The daemon
224        /// must re-validate the resulting config (peer input is untrusted)
225        /// and refuses with [`RpcErrorCode::InvalidConfig`] when it does not
226        /// deserialize or does not normalize; nothing is written in either
227        /// case.
228        ///
229        /// A bare [`serde_json::Value`] and not a redacting newtype, unlike
230        /// [`Self::SetSheepEnv`]'s [`EnvValue`], and the asymmetry is
231        /// deliberate. `env` is the one field [`AppConfig`]'s own manual
232        /// `Debug` redacts; `cwd`, `script` and `args` are printed in the
233        /// clear by every request that already carries a whole config
234        /// ([`Self::Start`], [`Self::Add`], [`Self::ApplyConfig`]). A
235        /// newtype here would protect one copy of a value this enum prints
236        /// three other ways, which reads as a guarantee the wire does not
237        /// make. Widening that protection is a change to [`AppConfig`]'s
238        /// `Debug`, not to this field.
239        value: serde_json::Value,
240    },
241    /// One dog's `[app.dogs.<name>]` table, for every sheep carrying one.
242    ///
243    /// It reads the stored spec, which is in force because `dogs` is
244    /// [`ApplyGroup::Live`](crate::config::ApplyGroup::Live). A sheep with
245    /// no table for `dog` is absent. `dog` is self-declared, as it is for
246    /// [`Self::DogConfig`], so the scoping is a convenience: the boundary
247    /// is the socket.
248    ///
249    /// Answers [`Response::DogSheepSettings`] with an empty map when no
250    /// sheep names `dog`, never [`RpcErrorCode::NotFound`]: unlike
251    /// [`Self::SheepConfig`], this does not name one sheep that could be
252    /// missing.
253    DogSheepSettings {
254        /// The dog's own name, the config key.
255        dog: String,
256    },
257    /// Sets, replaces, or with `None` removes one dog's table on one sheep,
258    /// recorded as an operator override of the whole `dogs` field so the
259    /// `*` marker shows, and publishes `config.sheep.<dog>` so a running
260    /// dog re-reads its tables.
261    ///
262    /// A single dog's table, not the whole `dogs` map:
263    /// [`Self::SetSheepField`] refuses the key `dogs` and names this
264    /// request, the way it refuses `env`, since a whole-map write from a
265    /// pane editing one dog would overwrite a concurrent edit to another
266    /// dog's table on the same sheep.
267    ///
268    /// Answers [`Response::SheepDogSettingsSet`], [`RpcErrorCode::NotFound`]
269    /// when no sheep has `name`, and refuses a dog's own name the way
270    /// [`Self::SetSheepField`] does.
271    SetSheepDogSettings {
272        /// The sheep's name, not a selector, for [`Self::SheepConfig`]'s
273        /// reason.
274        name: String,
275        /// Which dog's table to write.
276        dog: String,
277        /// The new table, or `None` to remove it.
278        table: Option<DogTable>,
279    },
280    /// Replaces one dog's `[<name>]` section in `dogs.toml` and publishes
281    /// `config.dog.<name>` so a running dog re-reads it.
282    ///
283    /// The writing twin of [`Self::DogConfig`], which reads the same
284    /// section.
285    ///
286    /// Answers [`Response::DogConfigSet`].
287    SetDogConfig {
288        /// The dog's name, the config key.
289        name: String,
290        /// The whole section, as TOML text.
291        ///
292        /// [`DogSectionToml`], not a bare `String`, for the reason that
293        /// type's own doc gives: a section can hold a dog's credentials and
294        /// this is what keeps them out of a `{:?}` (IR-41).
295        toml: DogSectionToml,
296    },
297    /// A provider dog's values for one namespace and one environment.
298    ///
299    /// Replaces that pair rather than merging into it, so a key deleted at
300    /// the provider disappears here on the next push instead of lingering.
301    ///
302    /// `namespace` is the dog's own registered name. It is bookkeeping, not
303    /// authorization: `Hello::dog_name` is self-declared and nothing checks
304    /// it against the spawn. The boundary is the socket itself, which lives
305    /// under `$SHEP_HOME` at `0700`.
306    ///
307    /// The two names and every entry key are checked against
308    /// [`crate::secrets::is_name`], and a value against
309    /// [`crate::secrets::MAX_VALUE_BYTES`], the same cap the operator's own
310    /// store enforces. One offender refuses the whole push rather than
311    /// dropping its own entry, so a dog never reads `accepted` for a set
312    /// that was stored in part.
313    ///
314    /// Answers [`Response::SecretsPut`].
315    PutSecrets {
316        /// The dog's registered name.
317        namespace: String,
318        /// Which environment these values are for.
319        environment: String,
320        /// The values, keyed by secret name. [`EnvValue`] so a `{:?}` of
321        /// this request cannot print them.
322        entries: BTreeMap<String, EnvValue>,
323    },
324    /// Stop matching sheep (stay registered)
325    Stop {
326        /// Which sheep
327        selector: SelectorSpec,
328    },
329    /// Restart matching sheep
330    Restart {
331        /// Which sheep
332        selector: SelectorSpec,
333    },
334    /// Replace each matching sheep with a fresh instance of the same app, one
335    /// instance of an app at a time, so the app has a window in which it can
336    /// stay reachable across the swap
337    Reload {
338        /// Which sheep. No default: a reload replaces running processes.
339        selector: SelectorSpec,
340    },
341    /// Stop matching sheep and refuse to spawn them again until `Enable`.
342    ///
343    /// The mark belongs to the app, so a selector naming an instance (an id
344    /// or a slot) is widened to that instance's app. Answers
345    /// [`Response::Disabled`].
346    Disable {
347        /// Which sheep
348        selector: SelectorSpec,
349    },
350    /// Clear the disabled mark on matching sheep and start them.
351    ///
352    /// A selector matching a sheep that is not disabled changes nothing.
353    /// Answers [`Response::Enabled`].
354    Enable {
355        /// Which sheep
356        selector: SelectorSpec,
357    },
358    /// Stop + deregister matching sheep
359    Delete {
360        /// Which sheep
361        selector: SelectorSpec,
362    },
363    /// Set how many instances one app runs (see `shep stock`).
364    ///
365    /// Takes a name where every other verb takes a [`SelectorSpec`]:
366    /// `instances` is a per-app number and slots are allocated against the
367    /// same-name group, so a selector matching two apps could mean four of
368    /// each or four in total.
369    ///
370    /// The count is absolute: two operators sending `+2` against the same
371    /// app would get a number neither asked for.
372    Scale {
373        /// The app's name, exactly as its config spells it. Not a selector: no
374        /// `all`, no regex, no `fold:`.
375        name: String,
376        /// How many instances the app has when this returns. `0` is refused
377        /// with [`RpcErrorCode::InvalidConfig`]: `shep delete` is the verb
378        /// for removing an app.
379        count: u32,
380    },
381    /// Attach a short marker to `sheep` for `shep flock` to paint, or clear
382    /// it with `None`.
383    ///
384    /// By name, not a selector: a smit belongs to a sheep, and every
385    /// instance of that name shows it, one spawned after the paint included.
386    ///
387    /// Held in memory and scoped to the connection that sent it, so a
388    /// publisher republishes rather than publishing on change.
389    SetSmit {
390        /// Which sheep.
391        sheep: String,
392        /// The marker, or `None` to clear it.
393        smit: Option<Smit>,
394    },
395    /// Reopen every matched sheep's log files, for an external rotator that
396    /// has renamed them (`create`-mode rotation)
397    Reopen {
398        /// Which sheep
399        selector: SelectorSpec,
400    },
401    /// Empty every matched sheep's log files: flush what is still pending,
402    /// then truncate the recorded paths
403    Flush {
404        /// Which sheep. No default: this destroys log data.
405        selector: SelectorSpec,
406    },
407    /// Send a named action to every matched sheep over its shepherd channel
408    /// and report what each app says back (see `shep trigger`).
409    Trigger {
410        /// Which sheep. No default, matching every other verb that reaches
411        /// a running process.
412        selector: SelectorSpec,
413        /// The action name. Free-form: the daemon never declares, parses, or
414        /// validates it, and an app that does not recognize the name is
415        /// expected to say so in its own reply.
416        action: String,
417        /// Argument text, passed through to the app verbatim. One opaque
418        /// string, matching the shepherd channel's own `action` message this
419        /// becomes.
420        params: Option<String>,
421    },
422    /// Deliver one signal to every matched sheep's own process, never its
423    /// process group (see `shep signal`).
424    Signal {
425        /// Which sheep. No default, matching every other verb that reaches
426        /// a running process.
427        selector: SelectorSpec,
428        /// The signal's name, as
429        /// [`OperatorSignal`](crate::signals::OperatorSignal) spells it. The
430        /// `SIG` prefix and the case are both optional; a name outside the
431        /// grammar answers [`RpcErrorCode::InvalidConfig`].
432        signal: String,
433    },
434    /// Write one line to every matched sheep's stdin (see `shep whisper`).
435    SendLine {
436        /// Which sheep. No default, matching every other verb that reaches a
437        /// running process.
438        selector: SelectorSpec,
439        /// The line, without its terminator: the shepherd appends exactly
440        /// one `\n` when it writes.
441        ///
442        /// A line containing an embedded newline is refused
443        /// ([`RpcErrorCode::InvalidConfig`]): it would deliver two commands
444        /// where the operator typed one.
445        line: String,
446    },
447    /// Write the muster roll now, bypassing the snapshot writer's debounce
448    SaveRoll,
449    /// Assemble the flock from the muster roll on disk: start every app the
450    /// roll recorded running, leaving every app the flock already has exactly
451    /// as it stands
452    Muster,
453    /// Ask for one dog's `[dog.<name>]` section, as the dog itself parses it
454    ///
455    /// The text as the operator wrote it: a `{{secret:...}}` reference comes
456    /// back as a reference. An editor must use this one. Lookout reads the
457    /// section and writes the whole text back through [`Self::SetDogConfig`],
458    /// so a resolved read would put the credential into `dogs.toml`.
459    ///
460    /// Answers [`Response::DogSection`].
461    DogConfig {
462        /// The dog's name: the config key, not a selector
463        name: String,
464    },
465    /// Ask for one dog's section with every `{{secret:...}}` reference
466    /// replaced by its value, resolved in the shepherd's own environment
467    ///
468    /// For a dog that consumes its configuration. [`Self::DogConfig`] is
469    /// unchanged and stays the raw read. A reference that cannot resolve
470    /// fails the request with [`RpcErrorCode::InvalidConfig`], naming the
471    /// key path and the reference and never a value. `dogs.toml` is never
472    /// rewritten.
473    ///
474    /// Answers [`Response::DogSection`]. Not a protocol bump, as
475    /// [`Self::HostUsage`] was not: a shepherd that has never heard of this
476    /// one decodes [`Self::Unrecognized`] and refuses by name.
477    DogConfigResolved {
478        /// The dog's name: the config key, not a selector
479        name: String,
480    },
481    /// Start one dog now, marking it as coming from `source`
482    EnableDog {
483        /// The dog's name
484        name: String,
485        /// Where its binary comes from
486        source: DogSource,
487    },
488    /// Stop and deregister one dog
489    ///
490    /// Answers [`Response::Deleted`]: disabling deregisters exactly as
491    /// `Delete` does.
492    DisableDog {
493        /// The dog's name
494        name: String,
495    },
496    /// Ask which dogs this daemon has given up on, and which it is still
497    /// waiting to hear from (`shep daemon reload`).
498    ///
499    /// Read-only, and about this daemon's own handshakes: take the reading
500    /// after a reload, not before one. Never sent to an older daemon, on
501    /// [`Self::HandoverFitness`]'s terms.
502    ///
503    /// Answers [`Response::DogStaleness`].
504    DogStaleness,
505    /// Ask whether this daemon could hand its flock to a successor in place,
506    /// rather than stopping it and starting it again (`shep daemon reload`).
507    ///
508    /// Read-only: the handover itself is triggered by a signal, which reaches
509    /// a daemon that refuses the client at the handshake.
510    ///
511    /// Answers [`Response::HandoverFitness`]. A refusal is a feature the
512    /// running daemon cannot carry, not an error: the caller falls back to a
513    /// stop-and-start and prints the reason. Never sent to an older daemon:
514    /// shep-cli's `commands::daemon` gates it on the crate version the
515    /// handshake reported.
516    HandoverFitness,
517    /// Graceful daemon shutdown
518    KillDaemon,
519    /// Subscribe this connection to bus topics (glob patterns)
520    Subscribe {
521        /// Topic globs, e.g. `process.*`
522        topics: Vec<String>,
523    },
524    /// A request kind this build has not been taught.
525    ///
526    /// `#[serde(other)]`, which serde allows here because `Request` is
527    /// internally tagged and this variant carries nothing. The unknown
528    /// body's own fields are discarded: the only thing to do with a
529    /// request we cannot name is refuse it, and the refusal needs the
530    /// envelope's id rather than the body.
531    #[serde(other)]
532    Unrecognized,
533}
534
535#[cfg(test)]
536mod tests;