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 + deregister matching sheep
342 Delete {
343 /// Which sheep
344 selector: SelectorSpec,
345 },
346 /// Set how many instances one app runs (see `shep stock`).
347 ///
348 /// Takes a name where every other verb takes a [`SelectorSpec`]:
349 /// `instances` is a per-app number and slots are allocated against the
350 /// same-name group, so a selector matching two apps could mean four of
351 /// each or four in total.
352 ///
353 /// The count is absolute: two operators sending `+2` against the same
354 /// app would get a number neither asked for.
355 Scale {
356 /// The app's name, exactly as its config spells it. Not a selector: no
357 /// `all`, no regex, no `fold:`.
358 name: String,
359 /// How many instances the app has when this returns. `0` is refused
360 /// with [`RpcErrorCode::InvalidConfig`]: `shep delete` is the verb
361 /// for removing an app.
362 count: u32,
363 },
364 /// Attach a short marker to `sheep` for `shep flock` to paint, or clear
365 /// it with `None`.
366 ///
367 /// By name, not a selector: a smit belongs to a sheep, and every
368 /// instance of that name shows it, one spawned after the paint included.
369 ///
370 /// Held in memory and scoped to the connection that sent it, so a
371 /// publisher republishes rather than publishing on change.
372 SetSmit {
373 /// Which sheep.
374 sheep: String,
375 /// The marker, or `None` to clear it.
376 smit: Option<Smit>,
377 },
378 /// Reopen every matched sheep's log files, for an external rotator that
379 /// has renamed them (`create`-mode rotation)
380 Reopen {
381 /// Which sheep
382 selector: SelectorSpec,
383 },
384 /// Empty every matched sheep's log files: flush what is still pending,
385 /// then truncate the recorded paths
386 Flush {
387 /// Which sheep. No default: this destroys log data.
388 selector: SelectorSpec,
389 },
390 /// Send a named action to every matched sheep over its shepherd channel
391 /// and report what each app says back (see `shep trigger`).
392 Trigger {
393 /// Which sheep. No default, matching every other verb that reaches
394 /// a running process.
395 selector: SelectorSpec,
396 /// The action name. Free-form: the daemon never declares, parses, or
397 /// validates it, and an app that does not recognize the name is
398 /// expected to say so in its own reply.
399 action: String,
400 /// Argument text, passed through to the app verbatim. One opaque
401 /// string, matching the shepherd channel's own `action` message this
402 /// becomes.
403 params: Option<String>,
404 },
405 /// Deliver one signal to every matched sheep's own process, never its
406 /// process group (see `shep signal`).
407 Signal {
408 /// Which sheep. No default, matching every other verb that reaches
409 /// a running process.
410 selector: SelectorSpec,
411 /// The signal's name, as
412 /// [`OperatorSignal`](crate::signals::OperatorSignal) spells it. The
413 /// `SIG` prefix and the case are both optional; a name outside the
414 /// grammar answers [`RpcErrorCode::InvalidConfig`].
415 signal: String,
416 },
417 /// Write one line to every matched sheep's stdin (see `shep whisper`).
418 SendLine {
419 /// Which sheep. No default, matching every other verb that reaches a
420 /// running process.
421 selector: SelectorSpec,
422 /// The line, without its terminator: the shepherd appends exactly
423 /// one `\n` when it writes.
424 ///
425 /// A line containing an embedded newline is refused
426 /// ([`RpcErrorCode::InvalidConfig`]): it would deliver two commands
427 /// where the operator typed one.
428 line: String,
429 },
430 /// Write the muster roll now, bypassing the snapshot writer's debounce
431 SaveRoll,
432 /// Assemble the flock from the muster roll on disk: start every app the
433 /// roll recorded running, leaving every app the flock already has exactly
434 /// as it stands
435 Muster,
436 /// Ask for one dog's `[dog.<name>]` section, as the dog itself parses it
437 DogConfig {
438 /// The dog's name: the config key, not a selector
439 name: String,
440 },
441 /// Start one dog now, marking it as coming from `source`
442 EnableDog {
443 /// The dog's name
444 name: String,
445 /// Where its binary comes from
446 source: DogSource,
447 },
448 /// Stop and deregister one dog
449 ///
450 /// Answers [`Response::Deleted`]: disabling deregisters exactly as
451 /// `Delete` does.
452 DisableDog {
453 /// The dog's name
454 name: String,
455 },
456 /// Ask which dogs this daemon has given up on, and which it is still
457 /// waiting to hear from (`shep daemon reload`).
458 ///
459 /// Read-only, and about this daemon's own handshakes: take the reading
460 /// after a reload, not before one. Never sent to an older daemon, on
461 /// [`Self::HandoverFitness`]'s terms.
462 ///
463 /// Answers [`Response::DogStaleness`].
464 DogStaleness,
465 /// Ask whether this daemon could hand its flock to a successor in place,
466 /// rather than stopping it and starting it again (`shep daemon reload`).
467 ///
468 /// Read-only: the handover itself is triggered by a signal, which reaches
469 /// a daemon that refuses the client at the handshake.
470 ///
471 /// Answers [`Response::HandoverFitness`]. A refusal is a feature the
472 /// running daemon cannot carry, not an error: the caller falls back to a
473 /// stop-and-start and prints the reason. Never sent to an older daemon:
474 /// shep-cli's `commands::daemon` gates it on the crate version the
475 /// handshake reported.
476 HandoverFitness,
477 /// Graceful daemon shutdown
478 KillDaemon,
479 /// Subscribe this connection to bus topics (glob patterns)
480 Subscribe {
481 /// Topic globs, e.g. `process.*`
482 topics: Vec<String>,
483 },
484 /// A request kind this build has not been taught.
485 ///
486 /// `#[serde(other)]`, which serde allows here because `Request` is
487 /// internally tagged and this variant carries nothing. The unknown
488 /// body's own fields are discarded: the only thing to do with a
489 /// request we cannot name is refuse it, and the refusal needs the
490 /// envelope's id rather than the body.
491 #[serde(other)]
492 Unrecognized,
493}
494
495#[cfg(test)]
496mod tests {
497 use super::super::{Envelope, LineOutcome, LineReply, Response, SignalOutcome, SignalReply};
498 use super::*;
499 use crate::protocol::MIN_SUPPORTED;
500
501 /// A wire fixture row with no deadline, which every row but the first uses.
502 fn envelope(id: u64, body: Request) -> Envelope {
503 Envelope {
504 id,
505 deadline_ms: None,
506 body,
507 }
508 }
509
510 #[test]
511 fn a_signal_request_and_its_reply_round_trip() {
512 let request = Request::Signal {
513 selector: SelectorSpec::Name("web".to_string()),
514 signal: "SIGHUP".to_string(),
515 };
516 let json = serde_json::to_string(&request).unwrap();
517 assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
518
519 let reply = Response::Signalled(vec![
520 SignalReply {
521 id: 1,
522 name: "web".to_string(),
523 outcome: SignalOutcome::Delivered,
524 },
525 SignalReply {
526 id: 2,
527 name: "web".to_string(),
528 outcome: SignalOutcome::NotRunning,
529 },
530 SignalReply {
531 id: 3,
532 name: "api".to_string(),
533 outcome: SignalOutcome::Failed {
534 reason: "no such process".to_string(),
535 },
536 },
537 ]);
538 let json = serde_json::to_string(&reply).unwrap();
539 assert_eq!(serde_json::from_str::<Response>(&json).unwrap(), reply);
540 // The three tags, spelled out: a variant renamed in Rust changes
541 // these strings, compiles clean, and breaks a client matching on them.
542 assert!(json.contains(r#""kind":"delivered""#), "{json}");
543 assert!(json.contains(r#""kind":"not_running""#), "{json}");
544 assert!(json.contains(r#""kind":"failed""#), "{json}");
545 }
546
547 /// `instances` is a per-app number, so `shep stock /web.*/ 4` could mean
548 /// four each or four total.
549 #[test]
550 fn a_scale_request_names_one_app_and_a_count() {
551 let request = Request::Scale {
552 name: "web".to_string(),
553 count: 4,
554 };
555 let json = serde_json::to_string(&request).unwrap();
556 assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
557 assert!(json.contains(r#""kind":"scale""#), "{json}");
558 assert!(json.contains(r#""name":"web""#), "{json}");
559 // No `selector` key at all: this verb is not one of the
560 // selector-taking family.
561 assert!(!json.contains("selector"), "{json}");
562 }
563
564 #[test]
565 fn a_scaled_reply_carries_its_own_tag() {
566 let json = serde_json::to_string(&Response::Scaled(vec![])).unwrap();
567 assert_eq!(json, r#"{"kind":"scaled","data":[]}"#);
568 }
569
570 /// `Add` and `Start` carry byte-identical payloads and differ by their
571 /// `kind` alone.
572 #[test]
573 fn an_add_request_and_its_reply_round_trip() {
574 let request = Request::Add {
575 apps: vec![AppConfig::minimal("web", "./srv")],
576 };
577 let json = serde_json::to_string(&request).unwrap();
578 assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
579 assert!(json.contains(r#""kind":"add""#), "{json}");
580
581 let reply = Response::Added(vec![]);
582 let json = serde_json::to_string(&reply).unwrap();
583 assert_eq!(serde_json::from_str::<Response>(&json).unwrap(), reply);
584 assert!(json.contains(r#""kind":"added""#), "{json}");
585 }
586
587 /// `NotWritten`'s reason is the only thing separating "the app is not
588 /// reading its stdin" from "the pipe broke".
589 #[test]
590 fn a_send_line_request_and_its_reply_round_trip() {
591 let request = Request::SendLine {
592 selector: SelectorSpec::Name("repl".to_string()),
593 line: "reload-config".to_string(),
594 };
595 let json = serde_json::to_string(&request).unwrap();
596 assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
597
598 let reply = Response::SentLine(vec![
599 LineReply {
600 id: 1,
601 name: "repl".to_string(),
602 outcome: LineOutcome::Sent,
603 },
604 LineReply {
605 id: 2,
606 name: "web".to_string(),
607 outcome: LineOutcome::NoStdin,
608 },
609 LineReply {
610 id: 3,
611 name: "stuck".to_string(),
612 outcome: LineOutcome::NotWritten {
613 reason: "the app did not read its stdin within 2s".to_string(),
614 },
615 },
616 ]);
617 let json = serde_json::to_string(&reply).unwrap();
618 assert_eq!(serde_json::from_str::<Response>(&json).unwrap(), reply);
619 assert!(json.contains(r#""kind":"sent""#), "{json}");
620 assert!(json.contains(r#""kind":"no_stdin""#), "{json}");
621 assert!(json.contains("did not read its stdin"), "{json}");
622 }
623
624 #[test]
625 fn a_line_carrying_a_newline_is_still_one_field_on_the_wire() {
626 let request = Request::SendLine {
627 selector: SelectorSpec::All,
628 line: "a\nb".to_string(),
629 };
630 let json = serde_json::to_string(&request).unwrap();
631 // Escaped, not literal: the frame stays one JSON object. Refusing
632 // it is the daemon's job, not serde's.
633 assert!(json.contains(r#""line":"a\nb""#), "{json}");
634 assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
635 }
636
637 /// The wire shape, pinned the way every other variant's is.
638 #[test]
639 fn set_sheep_env_batch_wire_v8() {
640 let request = Request::SetSheepEnvBatch {
641 name: "web".to_string(),
642 entries: BTreeMap::from([("A".to_string(), EnvValue::from("1".to_string()))]),
643 force: true,
644 dry_run: false,
645 };
646 let json = serde_json::to_string(&request).unwrap();
647 assert_eq!(
648 json,
649 r#"{"kind":"set_sheep_env_batch","name":"web","entries":{"A":"1"},"force":true,"dry_run":false}"#
650 );
651 assert_eq!(serde_json::from_str::<Request>(&json).unwrap(), request);
652 }
653
654 /// Additive, so the floor does not move. Guards against a reflexive
655 /// raise. The ceiling is pinned once, by `protocol::tests`, and moves
656 /// for reasons unrelated to this variant.
657 #[test]
658 fn the_batch_variant_did_not_raise_the_floor() {
659 assert_eq!(MIN_SUPPORTED, 8);
660 }
661
662 #[test]
663 fn request_wire_snapshots() {
664 let requests = vec![
665 Envelope {
666 id: 1,
667 deadline_ms: Some(5000),
668 body: Request::Ping,
669 },
670 envelope(2, Request::ListFlock),
671 envelope(
672 3,
673 Request::Stop {
674 selector: SelectorSpec::Name("web".to_string()),
675 },
676 ),
677 envelope(
678 4,
679 Request::Start {
680 apps: vec![AppConfig::minimal("web", "./srv")],
681 },
682 ),
683 // `All` rather than a named sheep: the selector `shep reopen`
684 // sends when given no argument.
685 envelope(
686 5,
687 Request::Reopen {
688 selector: SelectorSpec::All,
689 },
690 ),
691 // The same selector as the row above, so the two log-plane rows
692 // differ by their `kind` and by nothing else.
693 envelope(
694 6,
695 Request::Flush {
696 selector: SelectorSpec::All,
697 },
698 ),
699 // The same selector as the `stop` row: `reload` under `stop`'s tag
700 // shows up here as two identical objects.
701 envelope(
702 7,
703 Request::Reload {
704 selector: SelectorSpec::Name("web".to_string()),
705 },
706 ),
707 // `action`/`params` match channel.rs's with-params fixture
708 // verbatim, so a trigger reads the same at every hop.
709 envelope(
710 8,
711 Request::Trigger {
712 selector: SelectorSpec::Name("web".to_string()),
713 action: "set-log-level".to_string(),
714 params: Some("debug".to_string()),
715 },
716 ),
717 // A fieldless verb: a bare `{"kind":"..."}` with no `selector` key.
718 envelope(9, Request::SaveRoll),
719 // Paired with the `save_roll` row: they differ by their `kind` alone.
720 envelope(10, Request::Muster),
721 // The three dog verbs. `enable_dog` and `disable_dog` differ by
722 // their `kind` and by `source` alone.
723 envelope(
724 11,
725 Request::DogConfig {
726 name: "bark".to_string(),
727 },
728 ),
729 envelope(
730 12,
731 Request::EnableDog {
732 name: "metrics".to_string(),
733 source: DogSource::BuiltIn,
734 },
735 ),
736 envelope(
737 13,
738 Request::DisableDog {
739 name: "metrics".to_string(),
740 },
741 ),
742 // `Id`, `Regex` and `Fold` are three newtypes the wire tells apart
743 // only by their `kind` tag: a `Fold` under `regex`'s tag turns
744 // `shep restart fold:api` into a regex match.
745 envelope(
746 14,
747 Request::Describe {
748 selector: SelectorSpec::Id(7),
749 },
750 ),
751 envelope(
752 15,
753 Request::Describe {
754 selector: SelectorSpec::Regex("^web-".to_string()),
755 },
756 ),
757 envelope(
758 16,
759 Request::Describe {
760 selector: SelectorSpec::Fold("api".to_string()),
761 },
762 ),
763 // `SIGHUP` rather than `SIGTERM`: the stop ladder already sends
764 // TERM, so a TERM fixture could not tell the two frames apart.
765 envelope(
766 17,
767 Request::Signal {
768 selector: SelectorSpec::Name("web".to_string()),
769 signal: "SIGHUP".to_string(),
770 },
771 ),
772 // The one verb here whose body has no `selector` key.
773 envelope(
774 18,
775 Request::Scale {
776 name: "web".to_string(),
777 count: 4,
778 },
779 ),
780 // The line carries no terminator on the wire, since the shepherd
781 // appends it.
782 envelope(
783 19,
784 Request::SendLine {
785 selector: SelectorSpec::All,
786 line: "reload-config".to_string(),
787 },
788 ),
789 // Both halves of the `Option` are pinned, a paint and a clear, so a
790 // dog author does not have to guess the clear frame's shape.
791 envelope(
792 20,
793 Request::SetSmit {
794 sheep: "web".to_string(),
795 smit: Some(
796 "\u{25b2} main@a1b2c3"
797 .parse()
798 .expect("the reference smit is valid"),
799 ),
800 },
801 ),
802 envelope(
803 21,
804 Request::SetSmit {
805 sheep: "web".to_string(),
806 smit: None,
807 },
808 ),
809 // An empty `apps`: `start`'s row already pins the payload type, so
810 // this row's own are the tag and the key the list travels under.
811 envelope(22, Request::ConfigDrift { apps: Vec::new() }),
812 // The only struct-shaped `SelectorSpec` variant, so the only place
813 // `"kind":"instance"` and the `slot` key are pinned.
814 envelope(
815 23,
816 Request::Restart {
817 selector: SelectorSpec::Instance {
818 name: "web".to_string(),
819 slot: 2,
820 },
821 },
822 ),
823 // The one request an older daemon must never be sent: shep-cli
824 // gates it on the daemon's crate version.
825 envelope(24, Request::HandoverFitness),
826 // The second request gated on the daemon's crate version.
827 envelope(25, Request::DogStaleness),
828 // The only request carrying a `DeclaredApp`: a merge keys on what a
829 // document claimed. `declared_env` is non-empty to show it holds
830 // env key names and no env value, and `reset` is pinned at a
831 // non-default depth.
832 envelope(
833 26,
834 Request::ApplyConfig {
835 apps: vec![DeclaredApp {
836 config: AppConfig::minimal("web", "./srv"),
837 declared: ["name", "script"]
838 .iter()
839 .map(|k| (*k).to_string())
840 .collect(),
841 declared_env: ["DATABASE_URL"].iter().map(|k| (*k).to_string()).collect(),
842 }],
843 reset: ResetDepth::Policy,
844 },
845 ),
846 // The same app as the `start` row above: the two differ by their
847 // `kind` alone, so a mis-tagged `add` shows up as two identical
848 // objects.
849 envelope(
850 27,
851 Request::Add {
852 apps: vec![AppConfig::minimal("web", "./srv")],
853 },
854 ),
855 // The four config-pane requests. `SheepConfig` takes a name
856 // rather than a selector, like `Scale` and `SetSmit` above and
857 // for their reason: a pane edits one sheep.
858 envelope(
859 28,
860 Request::SheepConfig {
861 name: "web".to_string(),
862 },
863 ),
864 // `value` is pinned as `Some`, because the `None` spelling is
865 // what removes the key, and a reader that guessed the two apart
866 // wrongly would delete an operator's env instead of setting it.
867 // The value is a placeholder, not a secret: this is the one
868 // request in the enum that carries an env value at all, and it
869 // travels in one direction only, nothing ever reads it back.
870 envelope(
871 29,
872 Request::SetSheepEnv {
873 name: "web".to_string(),
874 key: "DATABASE_URL".to_string(),
875 value: Some("postgres://localhost/app".to_string().into()),
876 },
877 ),
878 // `SetSheepEnv`'s twin for everything that is not `env`, and
879 // pinned beside it: the two are one letter apart in the tag and
880 // a reader that crossed them would write a config field into an
881 // env map. `value` is a bare JSON value rather than a string,
882 // which is the half a hand-written reader gets wrong: an
883 // integer field is an integer here, not `"32"`.
884 envelope(
885 30,
886 Request::SetSheepField {
887 name: "web".to_string(),
888 key: "max_restarts".to_string(),
889 value: serde_json::json!(32),
890 },
891 ),
892 // The second request carrying a `DogSectionToml`, and pinned
893 // beside its reader: `DogConfig` asks for a section and this
894 // writes one back, so the two have to agree about the shape a
895 // section takes on the wire.
896 envelope(
897 31,
898 Request::SetDogConfig {
899 name: "bark".to_string(),
900 toml: "debounce = \"30s\"\n".to_string().into(),
901 },
902 ),
903 // The one request a provider dog sends, and the row that pins
904 // what `EnvValue` costs the wire: `entries` is a plain object
905 // of strings, so a dog written against this fixture in another
906 // language needs no newtype of its own.
907 envelope(
908 32,
909 Request::PutSecrets {
910 namespace: "vercel".to_string(),
911 environment: "production".to_string(),
912 entries: BTreeMap::from([(
913 "API_KEY".to_string(),
914 EnvValue::from("sk_live_placeholder".to_string()),
915 )]),
916 },
917 ),
918 // `SetSheepEnvBatch`'s own doc comment calls this the densest
919 // run of secrets on the wire, so it gets two entries rather
920 // than one: a single-entry map would not distinguish an object
921 // from a map with one key. `force` and `dry_run` are both
922 // pinned away from their default so a silent default flip on
923 // either field shows up here.
924 envelope(
925 33,
926 Request::SetSheepEnvBatch {
927 name: "web".to_string(),
928 entries: BTreeMap::from([
929 (
930 "DATABASE_URL".to_string(),
931 EnvValue::from("postgres://localhost/app".to_string()),
932 ),
933 (
934 "API_KEY".to_string(),
935 EnvValue::from("sk_live_placeholder".to_string()),
936 ),
937 ]),
938 force: true,
939 dry_run: true,
940 },
941 ),
942 envelope(34, Request::HostUsage),
943 envelope(
944 35,
945 Request::DogSheepSettings {
946 dog: "jobs".to_string(),
947 },
948 ),
949 // The table carries two keys, one nested, so a reader sees a
950 // table survives the hop through this enum without flattening.
951 envelope(
952 36,
953 Request::SetSheepDogSettings {
954 name: "web".to_string(),
955 dog: "jobs".to_string(),
956 table: Some(DogTable::from(serde_json::Map::from_iter([
957 ("concurrency".to_string(), serde_json::json!(2)),
958 (
959 "hours".to_string(),
960 serde_json::json!({ "start": "09:00:00" }),
961 ),
962 ]))),
963 },
964 ),
965 envelope(
966 37,
967 Request::SetSheepDogSettings {
968 name: "web".to_string(),
969 dog: "jobs".to_string(),
970 table: None,
971 },
972 ),
973 ];
974 insta::assert_json_snapshot!("request_wire_v11", requests);
975 }
976
977 #[test]
978 fn the_dog_verbs_serialize_snake_case_with_their_payloads_under_data() {
979 assert_eq!(
980 serde_json::to_string(&Request::DogConfig {
981 name: "bark".to_string()
982 })
983 .unwrap(),
984 r#"{"kind":"dog_config","name":"bark"}"#
985 );
986 assert_eq!(
987 serde_json::to_string(&Request::DisableDog {
988 name: "bark".to_string()
989 })
990 .unwrap(),
991 r#"{"kind":"disable_dog","name":"bark"}"#
992 );
993 let section = Response::DogSection {
994 toml: "port = 9615\n".to_string().into(),
995 };
996 let wire = r#"{"kind":"dog_section","data":{"toml":"port = 9615\n"}}"#;
997 assert_eq!(serde_json::to_string(§ion).unwrap(), wire);
998 assert_eq!(serde_json::from_str::<Response>(wire).unwrap(), section);
999 }
1000}