use std::collections::HashMap;
use sozu_command::{
protected_transaction::{Commit, Payload, WorkerStore},
proto::command::{
HealthCheckConfig, ProtectedHealthCheckOrder, ProtectedHealthCheckPhase,
ProtectedHealthCheckReply, SetHealthCheck, WorkerResponse, request::RequestType,
response_content::ContentType,
},
secret::{ProtectedHeaderSet, ProtectedHeaderVersion, ProtectedHealthCheck},
state::ConfigState,
};
use crate::backends::BackendMap;
struct Rollback {
state: Option<HealthCheckConfig>,
backend: Option<HealthCheckConfig>,
headers: Option<ProtectedHeaderSet>,
}
#[derive(Default)]
pub struct ProtectedWorker {
store: WorkerStore,
rollbacks: HashMap<(String, ProtectedHeaderVersion), Rollback>,
}
impl std::fmt::Debug for ProtectedWorker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProtectedWorker")
.field("store", &self.store)
.field("rollbacks", &self.rollbacks.len())
.finish()
}
}
fn reply(
request_id: &str,
phase: ProtectedHealthCheckPhase,
cluster: &str,
version: u64,
) -> WorkerResponse {
WorkerResponse::ok_with_content(
request_id.to_owned(),
ContentType::ProtectedHealthCheckReply(ProtectedHealthCheckReply {
phase: phase as i32,
cluster_id: cluster.to_owned(),
version,
})
.into(),
)
}
fn refuse(request_id: &str, reason: &'static str) -> WorkerResponse {
WorkerResponse::error(request_id.to_owned(), reason)
}
impl ProtectedWorker {
pub fn handle(
&mut self,
request_id: &str,
request: &mut RequestType,
state: &mut ConfigState,
backends: &mut BackendMap,
) -> Option<WorkerResponse> {
match request {
RequestType::QueryProtectedHealthCheckSupport(_) => Some(reply(
request_id,
ProtectedHealthCheckPhase::ProtectedHealthCheckCapability,
"",
0,
)),
RequestType::ProtectedHealthCheckOrder(order) => {
Some(self.order(request_id, order, state, backends))
}
_ => None,
}
}
pub fn forget_cluster(&mut self, cluster: &str) {
self.store.remove_cluster(cluster);
self.rollbacks.retain(|(held, _), _| held != cluster);
}
fn order(
&mut self,
request_id: &str,
order: &mut ProtectedHealthCheckOrder,
state: &mut ConfigState,
backends: &mut BackendMap,
) -> WorkerResponse {
let Ok(phase) = ProtectedHealthCheckPhase::try_from(order.phase) else {
return refuse(request_id, "unknown protected health check phase");
};
let version = ProtectedHeaderVersion::from_wire(order.version);
let cluster = order.cluster_id.to_owned();
let outcome = match phase {
ProtectedHealthCheckPhase::ProtectedHealthCheckCapability => {
Err("a capability query is not an order")
}
ProtectedHealthCheckPhase::ProtectedHealthCheckPrepare => {
self.prepare(order, version, state)
}
ProtectedHealthCheckPhase::ProtectedHealthCheckCommit => {
self.commit(&cluster, version, state, backends)
}
ProtectedHealthCheckPhase::ProtectedHealthCheckAbort => {
self.abort(&cluster, version, state, backends);
Ok(())
}
ProtectedHealthCheckPhase::ProtectedHealthCheckRelease => {
self.store.release(&cluster, version);
self.rollbacks.remove(&(cluster.to_owned(), version));
Ok(())
}
};
match outcome {
Ok(()) => reply(request_id, phase, &cluster, order.version),
Err(reason) => refuse(request_id, reason),
}
}
fn prepare(
&mut self,
order: &mut ProtectedHealthCheckOrder,
version: ProtectedHeaderVersion,
state: &ConfigState,
) -> Result<(), &'static str> {
let prepare = order
.prepare
.as_mut()
.ok_or("a prepare order carries no health check")?;
if prepare.cluster_id != order.cluster_id {
return Err("the prepared cluster is not the ordered one");
}
let protected =
ProtectedHealthCheck::from_trusted(prepare).map_err(|error| match error {
sozu_command::secret::ProtectedHealthCheckError::Invalid(reason) => reason,
_ => "the protected health check order is invalid",
})?;
if !state.clusters.contains_key(protected.cluster_id()) {
return Err("the cluster is not known to this worker");
}
let metadata = RequestType::SetHealthCheck(SetHealthCheck {
cluster_id: protected.cluster_id().to_owned(),
config: protected.config().to_owned(),
});
if state.health_check_port_unix_conflict(&metadata).is_some() {
return Err("a health check port and a unix backend cannot share a cluster");
}
self.store
.prepare(
protected.cluster_id(),
version,
Some(Payload {
config: protected.config().to_owned(),
headers: protected.headers().to_owned(),
}),
)
.map_err(|_| "the protected health check version is stale")
}
fn commit(
&mut self,
cluster: &str,
version: ProtectedHeaderVersion,
state: &mut ConfigState,
backends: &mut BackendMap,
) -> Result<(), &'static str> {
if self.rollbacks.contains_key(&(cluster.to_owned(), version)) {
return Ok(());
}
match self.store.commit(cluster, version) {
Commit::Applied => {}
Commit::Superseded => return Err("a newer protected health check is already applied"),
Commit::Unknown => return Err("the protected health check version was not staged"),
}
let Some(payload) = self.store.effective(cluster).cloned() else {
self.store.abort(cluster, version);
return Err("the protected health check version is a removal");
};
let rollback = Rollback {
state: state
.clusters
.get(cluster)
.and_then(|held| held.health_check.to_owned()),
backend: backends.health_check_configs.get(cluster).cloned(),
headers: backends
.protected_health_check_headers
.get(cluster)
.cloned(),
};
let applied = state.dispatch(
&RequestType::SetHealthCheck(SetHealthCheck {
cluster_id: cluster.to_owned(),
config: payload.config.to_owned(),
})
.into(),
);
if applied.is_err() {
self.store.abort(cluster, version);
self.store.remove_cluster(cluster);
return Err("the worker state refused the protected health check");
}
backends.set_protected_health_check(cluster, payload.config, payload.headers);
self.rollbacks
.insert((cluster.to_owned(), version), rollback);
Ok(())
}
fn abort(
&mut self,
cluster: &str,
version: ProtectedHeaderVersion,
state: &mut ConfigState,
backends: &mut BackendMap,
) {
self.store.abort(cluster, version);
if let Some(rollback) = self.rollbacks.remove(&(cluster.to_owned(), version)) {
if let Some(held) = state.clusters.get_mut(cluster) {
held.health_check = rollback.state;
}
backends.restore_health_check(cluster, rollback.backend, rollback.headers);
self.store.remove_cluster(cluster);
}
}
}