huskies: merge 1173 story Identity-aware fleet checks: cryptographic node identity in upgrade and health probes

This commit is contained in:
Huskies Agent
2026-07-16 13:26:03 +00:00
parent 489c415fd9
commit 0ac68afa4c
14 changed files with 894 additions and 30 deletions
+51
View File
@@ -40,6 +40,14 @@ pub struct ProjectEntry {
/// commands can route to the correct directory without re-deriving it.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host_path: Option<String>,
/// Ed25519 node ID (hex pubkey) this sled is expected to answer as.
///
/// Captured automatically from the first successful `/identity` probe
/// (story 1173) when absent, then checked on every subsequent upgrade and
/// health probe so a container swapped out from under the gateway is
/// detected even when its `/health` endpoint still reports ok.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_node_id: Option<String>,
}
impl ProjectEntry {
@@ -52,6 +60,7 @@ impl ProjectEntry {
auth_token: None,
ssh_port: None,
host_path: None,
expected_node_id: None,
}
}
@@ -223,6 +232,7 @@ auth_token = "secret"
auth_token: Some("secret".into()),
ssh_port: None,
host_path: None,
expected_node_id: None,
},
);
let config = GatewayConfig {
@@ -258,6 +268,7 @@ auth_token = "secret"
auth_token: Some("tok".into()),
ssh_port: None,
host_path: None,
expected_node_id: None,
},
);
assert_eq!(validate_project_exists(&projects, "ws").unwrap(), "");
@@ -278,6 +289,7 @@ auth_token = "secret"
auth_token: Some("tok".into()),
ssh_port: None,
host_path: None,
expected_node_id: None,
};
assert!(!e.has_url());
}
@@ -321,6 +333,7 @@ auth_token = "secret"
auth_token: Some("mysecret".into()),
ssh_port: None,
host_path: None,
expected_node_id: None,
};
let mut projects = BTreeMap::new();
projects.insert("myproj".into(), entry);
@@ -347,6 +360,7 @@ auth_token = "secret"
auth_token: None,
ssh_port: Some(2201),
host_path: None,
expected_node_id: None,
};
let mut projects = BTreeMap::new();
projects.insert("myproj".into(), entry);
@@ -364,6 +378,43 @@ auth_token = "secret"
);
}
#[test]
fn expected_node_id_roundtrips_and_is_omitted_when_none() {
let with_id = ProjectEntry {
url: Some("http://127.0.0.1:3101".into()),
auth_token: None,
ssh_port: None,
host_path: None,
expected_node_id: Some("ab".repeat(32)),
};
let mut projects = BTreeMap::new();
projects.insert("p".into(), with_id);
let config = GatewayConfig {
projects,
sled_tokens: BTreeMap::new(),
};
let toml_str = toml::to_string_pretty(&config).unwrap();
assert!(toml_str.contains("expected_node_id"));
let parsed: GatewayConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(
parsed.projects["p"].expected_node_id.as_deref(),
Some("ab".repeat(32).as_str())
);
let without_id = ProjectEntry::with_url("http://127.0.0.1:3101");
let mut projects2 = BTreeMap::new();
projects2.insert("p".into(), without_id);
let config2 = GatewayConfig {
projects: projects2,
sled_tokens: BTreeMap::new(),
};
let toml_str2 = toml::to_string_pretty(&config2).unwrap();
assert!(
!toml_str2.contains("expected_node_id"),
"expected_node_id should be omitted when None: {toml_str2}"
);
}
#[test]
fn ssh_port_none_is_omitted_from_toml() {
let entry = ProjectEntry::with_url("http://127.0.0.1:3101");