Bind relay restart override to its own active maintenance role
This commit is contained in:
@@ -50,7 +50,7 @@ fn owned_record(path: &Path, limit: u64, exact_private: bool) -> Result<Vec<u8>>
|
|||||||
);
|
);
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
/// The sole admitted drop-in is the currently held Indee API restart fence.
|
/// Only the currently held Indee API or relay restart fence is admitted.
|
||||||
/// This never grants ownership to arbitrary operator or historical overrides.
|
/// This never grants ownership to arbitrary operator or historical overrides.
|
||||||
fn validate_maintenance_override(
|
fn validate_maintenance_override(
|
||||||
data: &Path,
|
data: &Path,
|
||||||
@@ -62,10 +62,12 @@ fn validate_maintenance_override(
|
|||||||
if dropins.is_empty() {
|
if dropins.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
anyhow::ensure!(
|
let obligation = match name {
|
||||||
name == "indeedhub-api" && restart == "no",
|
"indeedhub-api" => "api_restart_override",
|
||||||
"External service override refused"
|
"indeedhub-relay" => "relay_restart_override",
|
||||||
);
|
_ => anyhow::bail!("External service override refused"),
|
||||||
|
};
|
||||||
|
anyhow::ensure!(restart == "no", "External service override refused");
|
||||||
owned_directory(data)?;
|
owned_directory(data)?;
|
||||||
for relative in [
|
for relative in [
|
||||||
"update-transactions",
|
"update-transactions",
|
||||||
@@ -77,7 +79,7 @@ fn validate_maintenance_override(
|
|||||||
owned_directory(&data.join(relative))?;
|
owned_directory(&data.join(relative))?;
|
||||||
}
|
}
|
||||||
let operation = String::from_utf8(owned_record(
|
let operation = String::from_utf8(owned_record(
|
||||||
&data.join("update-transactions/holds/indeedhub-api"),
|
&data.join("update-transactions/holds").join(name),
|
||||||
128,
|
128,
|
||||||
false,
|
false,
|
||||||
)?)?;
|
)?)?;
|
||||||
@@ -131,17 +133,17 @@ fn validate_maintenance_override(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|m| m["original"]["name"] == name)
|
.filter(|m| m["original"]["name"] == name)
|
||||||
.collect();
|
.collect();
|
||||||
anyhow::ensure!(matching.len() == 1, "Ambiguous native API identity");
|
anyhow::ensure!(matching.len() == 1, "Ambiguous native member identity");
|
||||||
let original = &matching[0]["original"];
|
let original = &matching[0]["original"];
|
||||||
let members = journal["original_members"]
|
let members = journal["original_members"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.context("Missing maintenance members")?;
|
.context("Missing maintenance members")?;
|
||||||
let matching: Vec<_> = members.iter().filter(|m| m["name"] == name).collect();
|
let matching: Vec<_> = members.iter().filter(|m| m["name"] == name).collect();
|
||||||
anyhow::ensure!(matching.len() == 1, "Ambiguous maintenance API identity");
|
anyhow::ensure!(matching.len() == 1, "Ambiguous maintenance member identity");
|
||||||
let member = matching[0];
|
let member = matching[0];
|
||||||
let body = original["body"]
|
let body = original["body"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.context("Missing original API unit")?;
|
.context("Missing original managed unit")?;
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
["container_id", "image", "config_sha256"]
|
["container_id", "image", "config_sha256"]
|
||||||
.iter()
|
.iter()
|
||||||
@@ -154,10 +156,10 @@ fn validate_maintenance_override(
|
|||||||
&& original["running"].is_boolean()
|
&& original["running"].is_boolean()
|
||||||
&& member["running"] == original["running"]
|
&& member["running"] == original["running"]
|
||||||
&& member["unit_sha256"] == hex::encode(Sha256::digest(body.as_bytes())),
|
&& member["unit_sha256"] == hex::encode(Sha256::digest(body.as_bytes())),
|
||||||
"Maintenance API binding changed"
|
"Maintenance member binding changed"
|
||||||
);
|
);
|
||||||
let bytes = format!("# Archipelago maintenance operation {operation}\n[Service]\nRestart=no\n");
|
let bytes = format!("# Archipelago maintenance operation {operation}\n[Service]\nRestart=no\n");
|
||||||
let saved = &journal["api_restart_override"];
|
let saved = &journal[obligation];
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
saved["operation_id"].as_str() == Some(operation.as_str())
|
saved["operation_id"].as_str() == Some(operation.as_str())
|
||||||
&& saved["released"] == false
|
&& saved["released"] == false
|
||||||
@@ -165,16 +167,14 @@ fn validate_maintenance_override(
|
|||||||
"Maintenance restart override obligation changed"
|
"Maintenance restart override obligation changed"
|
||||||
);
|
);
|
||||||
owned_directory(runtime)?;
|
owned_directory(runtime)?;
|
||||||
for relative in [
|
for relative in ["systemd", "systemd/user"] {
|
||||||
"systemd",
|
|
||||||
"systemd/user",
|
|
||||||
"systemd/user/indeedhub-api.service.d",
|
|
||||||
] {
|
|
||||||
owned_directory(&runtime.join(relative))?;
|
owned_directory(&runtime.join(relative))?;
|
||||||
}
|
}
|
||||||
let expected = runtime
|
let service_dir = runtime
|
||||||
.join("systemd/user/indeedhub-api.service.d")
|
.join("systemd/user")
|
||||||
.join(format!("zz-archipelago-maintenance-{operation}.conf"));
|
.join(format!("{name}.service.d"));
|
||||||
|
owned_directory(&service_dir)?;
|
||||||
|
let expected = service_dir.join(format!("zz-archipelago-maintenance-{operation}.conf"));
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
expected.to_str() == Some(dropins),
|
expected.to_str() == Some(dropins),
|
||||||
"Unexpected or additional service override"
|
"Unexpected or additional service override"
|
||||||
@@ -863,6 +863,7 @@ impl<B: DrainBarrier> Supervisor for SystemdSupervisor<B> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
struct OverrideFixture {
|
struct OverrideFixture {
|
||||||
|
name: &'static str,
|
||||||
_root: tempfile::TempDir,
|
_root: tempfile::TempDir,
|
||||||
data: PathBuf,
|
data: PathBuf,
|
||||||
runtime: PathBuf,
|
runtime: PathBuf,
|
||||||
@@ -876,6 +877,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
impl OverrideFixture {
|
impl OverrideFixture {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
Self::for_role("indeedhub-api", "api_restart_override")
|
||||||
|
}
|
||||||
|
fn for_role(name: &'static str, obligation: &str) -> Self {
|
||||||
let root = tempfile::tempdir().unwrap();
|
let root = tempfile::tempdir().unwrap();
|
||||||
let data = root.path().join("data");
|
let data = root.path().join("data");
|
||||||
let runtime = root.path().join("runtime");
|
let runtime = root.path().join("runtime");
|
||||||
@@ -888,26 +892,29 @@ mod tests {
|
|||||||
data.join("update-transactions/supervised"),
|
data.join("update-transactions/supervised"),
|
||||||
maintenance.clone(),
|
maintenance.clone(),
|
||||||
data.join("app-maintenance"),
|
data.join("app-maintenance"),
|
||||||
runtime.join("systemd/user/indeedhub-api.service.d"),
|
runtime
|
||||||
|
.join("systemd/user")
|
||||||
|
.join(format!("{name}.service.d")),
|
||||||
] {
|
] {
|
||||||
std::fs::create_dir_all(path).unwrap();
|
std::fs::create_dir_all(path).unwrap();
|
||||||
}
|
}
|
||||||
private_write(
|
private_write(
|
||||||
&data.join("update-transactions/holds/indeedhub-api"),
|
&data.join("update-transactions/holds").join(name),
|
||||||
operation.as_bytes(),
|
operation.as_bytes(),
|
||||||
);
|
);
|
||||||
private_write(
|
private_write(
|
||||||
&data.join("app-maintenance/indeedhub"),
|
&data.join("app-maintenance/indeedhub"),
|
||||||
operation.as_bytes(),
|
operation.as_bytes(),
|
||||||
);
|
);
|
||||||
let body = "[Container]\nContainerName=indeedhub-api\n";
|
let body = format!("[Container]\nContainerName={name}\n");
|
||||||
let original = serde_json::json!({"name":"indeedhub-api","container_id":"a".repeat(64),"image":"b".repeat(64),"config_sha256":"c".repeat(64),"body":body,"running":true});
|
let original = serde_json::json!({"name":name,"container_id":"a".repeat(64),"image":"b".repeat(64),"config_sha256":"c".repeat(64),"body":body,"running":true});
|
||||||
let native = serde_json::json!({"schema":2,"id":operation,"package":"indeedhub","phase":"Restoring","cleanup_done":false,"members":[{"original":original}]});
|
let native = serde_json::json!({"schema":2,"id":operation,"package":"indeedhub","phase":"Restoring","cleanup_done":false,"members":[{"original":original}]});
|
||||||
let bytes =
|
let bytes =
|
||||||
format!("# Archipelago maintenance operation {operation}\n[Service]\nRestart=no\n");
|
format!("# Archipelago maintenance operation {operation}\n[Service]\nRestart=no\n");
|
||||||
let journal = serde_json::json!({"operation_id":operation,"phase":"Recovering","original_members":[{"name":"indeedhub-api","container_id":original["container_id"],"image_id":original["image"],"config_sha256":original["config_sha256"],"unit_sha256":hex::encode(Sha256::digest(body.as_bytes())),"running":true}],"api_restart_override":{"operation_id":operation,"released":false,"sha256":hex::encode(Sha256::digest(bytes.as_bytes()))}});
|
let journal = serde_json::json!({"operation_id":operation,"phase":"Recovering","original_members":[{"name":name,"container_id":original["container_id"],"image_id":original["image"],"config_sha256":original["config_sha256"],"unit_sha256":hex::encode(Sha256::digest(body.as_bytes())),"running":true}],(obligation):{"operation_id":operation,"released":false,"sha256":hex::encode(Sha256::digest(bytes.as_bytes()))}});
|
||||||
let override_path = runtime
|
let override_path = runtime
|
||||||
.join("systemd/user/indeedhub-api.service.d")
|
.join("systemd/user")
|
||||||
|
.join(format!("{name}.service.d"))
|
||||||
.join(format!("zz-archipelago-maintenance-{operation}.conf"));
|
.join(format!("zz-archipelago-maintenance-{operation}.conf"));
|
||||||
let native_path = data
|
let native_path = data
|
||||||
.join("update-transactions/supervised")
|
.join("update-transactions/supervised")
|
||||||
@@ -917,6 +924,7 @@ mod tests {
|
|||||||
private_write(&native_path, &serde_json::to_vec(&native).unwrap());
|
private_write(&native_path, &serde_json::to_vec(&native).unwrap());
|
||||||
private_write(&journal_path, &serde_json::to_vec(&journal).unwrap());
|
private_write(&journal_path, &serde_json::to_vec(&journal).unwrap());
|
||||||
Self {
|
Self {
|
||||||
|
name,
|
||||||
_root: root,
|
_root: root,
|
||||||
data,
|
data,
|
||||||
runtime,
|
runtime,
|
||||||
@@ -929,7 +937,7 @@ mod tests {
|
|||||||
validate_maintenance_override(
|
validate_maintenance_override(
|
||||||
&self.data,
|
&self.data,
|
||||||
&self.runtime,
|
&self.runtime,
|
||||||
"indeedhub-api",
|
self.name,
|
||||||
self.override_path.to_str().unwrap(),
|
self.override_path.to_str().unwrap(),
|
||||||
"no",
|
"no",
|
||||||
)
|
)
|
||||||
@@ -971,6 +979,42 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
|
fn api_and_relay_overrides_cannot_borrow_each_others_obligation() {
|
||||||
|
for (name, own, other) in [
|
||||||
|
(
|
||||||
|
"indeedhub-api",
|
||||||
|
"api_restart_override",
|
||||||
|
"relay_restart_override",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"indeedhub-relay",
|
||||||
|
"relay_restart_override",
|
||||||
|
"api_restart_override",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let f = OverrideFixture::for_role(name, own);
|
||||||
|
f.validate().unwrap();
|
||||||
|
let bytes = std::fs::read(&f.journal_path).unwrap();
|
||||||
|
let mut journal: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||||
|
let value = journal.as_object_mut().unwrap().remove(own).unwrap();
|
||||||
|
journal[other] = value;
|
||||||
|
private_write(&f.journal_path, &serde_json::to_vec(&journal).unwrap());
|
||||||
|
assert!(f.validate().is_err());
|
||||||
|
private_write(&f.journal_path, &bytes);
|
||||||
|
let borrowed = f.override_path.to_str().unwrap().replace(
|
||||||
|
name,
|
||||||
|
if name == "indeedhub-api" {
|
||||||
|
"indeedhub-relay"
|
||||||
|
} else {
|
||||||
|
"indeedhub-api"
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
validate_maintenance_override(&f.data, &f.runtime, name, &borrowed, "no").is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
fn restart_override_requires_matching_live_native_and_controller_obligations() {
|
fn restart_override_requires_matching_live_native_and_controller_obligations() {
|
||||||
for (native, key, value) in [
|
for (native, key, value) in [
|
||||||
(
|
(
|
||||||
|
|||||||
Reference in New Issue
Block a user