host.sock: give HostResponse a wire-schema version, warn on mismatch
This commit is contained in:
parent
5640832116
commit
a32f37c3ba
2 changed files with 87 additions and 2 deletions
|
|
@ -517,8 +517,27 @@ pub struct HiveUrls {
|
|||
pub matrix: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
/// Wire-schema version for [`HostResponse`]. Bump only for a breaking
|
||||
/// change — removing, renaming, or retyping an existing field; adding a
|
||||
/// new `Option`/`skip_serializing_if` field (the pattern every field on
|
||||
/// this struct already follows) is not breaking and needs no bump. A
|
||||
/// client compares this against its own compiled-in copy of the constant
|
||||
/// and warns (does not fail the request) on mismatch — see
|
||||
/// `hivectl::client::version_mismatch_hint`, the one place that reads it.
|
||||
pub const HOST_SOCK_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HostResponse {
|
||||
/// Wire-schema version this response was built against, see
|
||||
/// [`HOST_SOCK_VERSION`]. `#[serde(default)]` so a pre-version daemon
|
||||
/// (or a hand-built test fixture) deserializes as `0` rather than
|
||||
/// failing to parse — `0` reads as "older than any real version" to a
|
||||
/// mismatch check, which is the correct answer for wire predating
|
||||
/// this field. Always populated on the write side: every constructor
|
||||
/// below routes through `Self::default()`, whose `Default` impl (not
|
||||
/// derived — see below) sets this to the current version.
|
||||
#[serde(default)]
|
||||
pub version: u32,
|
||||
pub ok: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
|
|
@ -568,6 +587,31 @@ pub struct HostResponse {
|
|||
pub quota: Option<Vec<QuotaRow>>,
|
||||
}
|
||||
|
||||
/// Hand-written rather than `#[derive(Default)]` for one reason:
|
||||
/// `version` must default to [`HOST_SOCK_VERSION`], not `u32::default()`
|
||||
/// (`0`). Every field below it is the same all-`None`/`false`/empty shape
|
||||
/// `derive(Default)` would have produced — only `version` differs — and
|
||||
/// every constructor in `impl HostResponse` builds through `..Self::default()`,
|
||||
/// so this is the one place that has to get it right.
|
||||
impl Default for HostResponse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: HOST_SOCK_VERSION,
|
||||
ok: false,
|
||||
error: None,
|
||||
agents: None,
|
||||
approvals: None,
|
||||
urls: None,
|
||||
agent_statuses: None,
|
||||
agent_exists: None,
|
||||
queued_dags: None,
|
||||
nodes: None,
|
||||
messages: Vec::new(),
|
||||
quota: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HostResponse {
|
||||
#[must_use]
|
||||
pub fn success() -> Self {
|
||||
|
|
|
|||
|
|
@ -69,12 +69,53 @@ pub async fn request(socket: &Path, req: HostRequest) -> Result<HostResponse> {
|
|||
bail!("server closed connection without responding");
|
||||
}
|
||||
let resp: HostResponse = serde_json::from_str(line.trim())?;
|
||||
if let Some(hint) = version_mismatch_hint(resp.version) {
|
||||
eprintln!("{hint}");
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
/// Message to print (stderr, non-fatal) when the daemon's `HostResponse`
|
||||
/// wire version differs from what this `hivectl` build was compiled
|
||||
/// against. `None` when they match. Split out from the print site so it's
|
||||
/// unit-testable, same pattern as `connect_hint` above — a mismatch is not
|
||||
/// a request failure (most fields still decode fine across a one-version
|
||||
/// skew), just something the operator should know before trusting a
|
||||
/// response that might be missing a field this build expects.
|
||||
fn version_mismatch_hint(daemon_version: u32) -> Option<String> {
|
||||
(daemon_version != hive_host_sock::HOST_SOCK_VERSION).then(|| {
|
||||
format!(
|
||||
"warning: hive-c0re host.sock wire version {daemon_version} does not match \
|
||||
hivectl's {} — upgrade hivectl or the daemon; some response fields may be \
|
||||
missing or misread",
|
||||
hive_host_sock::HOST_SOCK_VERSION
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ErrorKind, Path, connect_hint};
|
||||
use super::{ErrorKind, Path, connect_hint, version_mismatch_hint};
|
||||
|
||||
/// A daemon on the same version this `hivectl` was built against gets
|
||||
/// no warning — the common case, and the only one that must stay silent.
|
||||
#[test]
|
||||
fn matching_version_has_no_hint() {
|
||||
assert!(version_mismatch_hint(hive_host_sock::HOST_SOCK_VERSION).is_none());
|
||||
}
|
||||
|
||||
/// A mismatch names both versions, so the operator knows which side
|
||||
/// (hivectl or the daemon) is behind, not just that they disagree.
|
||||
#[test]
|
||||
fn mismatched_version_names_both_numbers() {
|
||||
let other = hive_host_sock::HOST_SOCK_VERSION + 1;
|
||||
let h = version_mismatch_hint(other).expect("versions differ");
|
||||
assert!(h.contains(&other.to_string()), "{h}");
|
||||
assert!(
|
||||
h.contains(&hive_host_sock::HOST_SOCK_VERSION.to_string()),
|
||||
"{h}"
|
||||
);
|
||||
}
|
||||
|
||||
fn hint(kind: ErrorKind) -> String {
|
||||
connect_hint(kind, Path::new("/run/hyperhive/host.sock"))
|
||||
|
|
|
|||
Loading…
Reference in a new issue