Deployment brake is a stop: DeploymentStop record, signed release path deleted, record v6 #4

Merged
eureka merged 2 commits from eureka-self/brake-is-a-stop into main 2026-11-13 17:43:21 +00:00
11 changed files with 1255 additions and 792 deletions

View file

@ -45,7 +45,6 @@ ExecStart=/usr/local/bin/idunn serve \
--idunn-identity-store /var/lib/gamecult/idunn/idunn-service-identity.cc \
--odin-correlation-store /var/lib/gamecult/odin/topology.cc \
--odin-trust-anchor /etc/gamecult/idunn/odin-topology-anchor.cc \
--deployment-brake-operator-anchor /etc/gamecult/idunn/deployment-brake-operator-anchor.cc \
--host-actuator-bind 10.77.0.1:17890 \
--source-uid 986 \
--source-gid 978

View file

@ -16,7 +16,6 @@ target="${2:-}"
compose=/srv/compose/yggdrasil-apps.yaml
brake_store=/var/lib/gamecult/idunn-authority/deployment-brake.cc
brake_lock=/var/lib/gamecult/idunn-authority/deployment-brake.cc.lock
brake_anchor=/etc/gamecult/idunn/deployment-brake-operator-anchor.cc
generation_lock=/var/lib/gamecult/idunn-authority/idunn-generation.lock
idunn_provision=/usr/local/bin/idunn-provision
@ -154,7 +153,7 @@ deployment_target_policy() {
esac
}
require_signed_deployment_grant() {
require_deployment_admission() {
source_commit="$3"
deployment_request_id="$4"
release_authority_id="$5"
@ -180,28 +179,17 @@ require_signed_deployment_grant() {
[ -z "$release_authority_sha256" ] || { echo "source-observed release cannot claim Bifrost authority digest" >&2; exit 1; }
require_root_owned_resolved_executable "$idunn_provision"
require_root_owned_regular_path "$brake_store"
require_root_owned_regular_path "$brake_lock"
require_root_owned_regular_path "$brake_anchor"
# This validation is the deployment-admission point. The signed expiry is a
# start-before deadline: it denies a later transaction, but does not become a
# timer-shaped veto over one already admitted. The root actuator remains the
# sole shared-lock holder through the synchronous consequence, so an explicit
# engage/release transition cannot race or rewrite that transaction.
exec 9<"$brake_lock"
# This validation is the deployment-admission point. An absent or clear stop
# admits; an engaged or unreadable one makes the status verb exit nonzero.
# The root actuator remains the sole shared-lock holder through the
# synchronous consequence, so an engage or clear cannot race or rewrite it.
# The lock file is created here when no store has ever been written.
exec 9>>"$brake_lock"
/usr/bin/flock --shared 9
now_unix_millis="$(/usr/bin/date +%s%3N)"
case "$now_unix_millis" in
''|*[!0-9]*) echo "root actuator clock did not produce Unix milliseconds" >&2; exit 1 ;;
esac
"$idunn_provision" deployment-brake-status \
--store "$brake_store" \
--operator-anchor "$brake_anchor" \
--runtime-id yggdrasil \
--release-id "$source_commit" \
--deployment-id "$deployment_request_id" \
--now-unix-millis "$now_unix_millis"
--runtime-id yggdrasil
if [ "$minimum_source_revision_required" = true ]; then
require_root_owned_resolved_executable /usr/local/bin/idunn
@ -322,7 +310,7 @@ case "$action:$target" in
manifest="/srv/odin/deploy-manifests/$target"
require_root_owned_regular_path "$manifest"
[ -x "$manifest" ] || { echo "root actuator manifest is not executable: $manifest" >&2; exit 1; }
require_signed_deployment_grant "$@"
require_deployment_admission "$@"
export IDUNN_ACTUATOR=1
export IDUNN_COMMAND_AUTHORITY=idunn-daemon
export IDUNN_SOURCE_COMMIT="$source_commit"

View file

@ -240,7 +240,7 @@ Fencing retries its phase forward on a capped backoff and never stops its live
candidate, since that is the only running copy of the admitted body; the
deadline opens the same incident. The one exception is yielding: a continuity
whose route proof has not succeeded gives way, through its own post-fencing
abort, to a deployment the deployment brake has released and that awaits the
abort, to a deployment the deployment stop admits and that awaits the
handover; a continuity that has proven its route never yields. The
`ContinuityExhausted` incident (six charged attempts in an hour,
`restarts_page`) pages and gates nothing. When continuity next acts is derived
@ -257,9 +257,14 @@ held until a lift succeeds. It is a migration with a deletion line (follow-up
still owe dependency failures not growing the streak and the dead-generation
route refresh.
The deployment brake gates changes to source, release, configuration, schema,
unit, or authority binding. The lifecycle brake separately gates restart of an
already-admitted incarnation. Neither target brake may gate Idunn itself or an
The deployment brake is a stop, not a per-deploy gate: it gates changes to
source, release, configuration, schema, unit, or authority binding only while
engaged. Its one reader and writer is `src/brake.rs` (`idunn.deployment_stop.v1`:
engaged or clear, who, why, when, sequence). An absent or clear store admits;
a store that is present but unreadable, foreign, for another runtime, a symlink
or owned by another uid fails closed. Nothing is signed, bound to a release or
consumed, and one clear admits every deploy until it is engaged again. The
lifecycle brake separately gates restart of an already-admitted incarnation. Neither target brake may gate Idunn itself or an
unrelated service.
If Odin is unavailable, Idunn preserves existing routes and continuity for

View file

@ -4,6 +4,15 @@ Status: target, Self, 2026-10-19 (campaign `idunn-deploy-provenance` in
Eureka's mind). It states the ends. The map will live in
`deploy-provenance-map.md`.
Live as of cut `brake-is-a-stop` (2026-11-13): the deployment brake is the
stop of invariant 3 and the signed per-deploy release is gone. `src/brake.rs`
reads and writes it; `idunn-provision deployment-brake-engage|clear|status`
take only `--store`, `--runtime-id` and `--reason`; the writer's login name and
the verb's clock are recorded; an absent or clear store admits. The
transaction stamps `deployment_admitted_at_unix_millis` (record v6). Still
planned: the typed authority provenance on the transaction (invariant 2), and a
steward-scoped store.
## Why
Operator, 2026-10-19:

View file

@ -48,8 +48,8 @@ and health. CultMesh carries typed projections between them.
scaling, restart, host reboot recovery, and crash recovery use the same
source-to-recipe-to-artifact provenance and the same distinct write-lease and
route-membership primitives.
Deployment may change that chain only while the deployment brake admits the
exact transaction. Continuity may restart only the already-admitted chain and
Deployment may change that chain only while the deployment stop admits it
(absent or clear; an engaged stop halts). Continuity may restart only the already-admitted chain and
is governed by a separate lifecycle brake.
- **Deletion line:** hard-coded target copies, raw `deploy_command` and
`restart_command` state, `sh -c` actuation, root Git inspection, staged
@ -209,8 +209,8 @@ non-regular carry source fails the deployment before anything moves.
What `current_link` names is observed from the symlink and confirmed by the
latest Admitted static transaction's `flip.release_id`; it is stored nowhere
else. A rollback is a flip back to `flip.previous_release_id`: release
directories stay, and Idunn prunes none. The brake is read exactly as for a
service, naming the binding's runtime id and the sealed release id. Release ids are per transaction, so
directories stay, and Idunn prunes none. The deployment stop is read exactly as for a
service, by the binding's runtime id. Release ids are per transaction, so
redeploying an unchanged tarball with a changed site file is a new release that
carries the new file, never an in-place re-carry of an existing one: the carry
is part of a release. If `current_link` is replaced by a directory or by a link
@ -230,9 +230,8 @@ transaction (`CommandKind::Rollback`) that reuses the previous release exactly:
the plan, sealed release and installed release are copied from the transaction
that flipped it, so there is no source, no build and no source cleanup, and it
creates no admitted generation. It starts at Sealing with that evidence and
reaches the same deployment brake as a deploy, which the operator releases for
the rollback transaction's id and the previous sealed release id; then
`advance_static` flips, with the same three durable steps, the same
reaches the same deployment stop as a deploy, which admits it unless engaged;
then `advance_static` flips, with the same three durable steps, the same
nothing-aborts-after-the-rename rule and the same stuck-flip behavior.
The previous release is derived, never stored: the newest Admitted static
@ -300,6 +299,31 @@ names both content addresses and the executable artifact digest. Present and
Ready remain Odin-owned correlations over independently verified runtime
evidence; the deployment-plan module deliberately does not model them.
## The deployment stop
`src/brake.rs` is the only reader and writer of the stop. A stop halts work
that has not crossed an irreversible step, and it is read again at each such
step: Sealing (`advance_sealing`), the static flip intent (`advance_static`) and
the entry into Fencing for a deploy (`advance_warming`). An engaged stop there
makes the transaction wait with a note naming who engaged it and why; nothing
aborts and nothing moves. After the flip intent is recorded, or once a deploy
has fenced, the transaction runs to its end: a stop halts before the
irreversible step, never mid-fence or mid-flip. Continuity is never gated.
The sequence of a stop record is positive and advances by one per verb; nothing
reads it for authority, so it is not validated further.
Absent means the store file is missing in an existing directory. Anything else
Idunn cannot judge (a symlink, a zero-length or undecodable file, another
runtime's record, another owner, an unsearchable or non-directory parent) reads
as unreadable and fails closed; the wait note carries the
`idunn-provision deployment-brake-clear` command that replaces it.
Record v6 (`deployment_admitted_at_unix_millis`, key 38) is forward-only: an
Idunn that predates v6 cannot read a migrated `control.cc`, so, as for v5 (see
`route-continuity-cut.md`, "An older Idunn refuses the v5 store"), take a
byte-compared pre-install backup of `control.cc` and `history.cc` with Idunn
stopped, and roll back by restoring the backup, never by swapping the binary.
## Initial drivers and replaceable boundary
The first Yggdrasil implementation uses:
@ -338,7 +362,7 @@ For a singleton incarnation:
declaration, binding, launch contract, and input digests.
5. Derive the release-bound Expected incarnation from the validated sealed
release and its exact compiled plan. Publish it only once the deployment
brake has admitted this exact transaction, under the candidate's own
stop has admitted this transaction (stamping `deployment_admitted_at_unix_millis`), under the candidate's own
incarnation key, beside whatever the target's admitted incarnation projects.
6. Issue one opaque runtime-instance activation bound to the Expected digest,
pass the exact Expected and activation documents through the actuator's
@ -361,8 +385,8 @@ For a singleton incarnation:
and stop the incumbent.
Sealing resolves the source, freezes it, runs every recipe step, and installs
the release before the deployment brake is consulted at all (`advance_sealing`).
The brake gates step 5, the first Verse-visible change: publishing the
the release before the deployment stop is consulted at all (`advance_sealing`).
The stop gates step 5, the first Verse-visible change: publishing the
candidate's Expected incarnation on the move into `Starting`. Build and test
already run unbraked, inside the sealing phase.
@ -393,7 +417,7 @@ identity, and route membership. Every restart receives a new runtime-instance
activation and, for a state writer, a new process-bound lease only after the new
warming presence is observed. A lease from the dead process is never reused. A
continuity restart does not fetch a branch, rebuild, rerun a migration, alter
the launch contract, or consume the deployment brake. An explicit lifecycle
the launch contract, or pass the deployment stop. An explicit lifecycle
brake may suspend this actuation without changing deployment authority.
Continuity never stops. Six charged attempts in an hour only open the
@ -426,7 +450,7 @@ the admitted body. Both operator cases open an `operator-required` incident
on.
The one yield: a continuity whose route proof has not succeeded gives way to a
deployment the deployment brake has released that awaits the handover of the
deployment the deployment stop admits that awaits the handover of the
target. It begins its own post-fencing abort and the deployment fences what is
admitted. A continuity that has proven its route is serving never yields.

View file

@ -1160,7 +1160,7 @@ target's generation or to its `TargetSupervision` ends that target's iteration:
- **Why no existing owner can serve:**
- The generation does not exist for a first deploy (the ruling).
- The transaction dies with each attempt.
- Brakes are operator-signed and are not Idunn's to write.
- The deployment brake is a stop that a steward or the operator writes (`src/brake.rs`); Idunn writes no brake.
- **What it replaces:** two carry compensators, `for_new_incarnation` and the backoff copy in
`from_transaction`, plus a history scan.

View file

@ -155,7 +155,7 @@ cleaned up.
`idunn redeploy --daemon …`. The installed binary has no `redeploy` command
(probe of its usage text), so the script is dead and is **not** the current
pattern. The current pattern is `idunn up` followed by
`idunn-provision deployment-brake-release`
`idunn-provision deployment-brake-clear` when a stop is engaged (an absent or clear store admits)
(`runbooks/idunn-host-raven.md`, "Deploy Muninn to Raven").
- F24. Build placement (`gamecult-ops/inventory.md:559-566`): build on or for the
target's host. Yggdrasil has 16 vCPU and 62 GiB and carries live services, so

513
src/brake.rs Normal file
View file

@ -0,0 +1,513 @@
//! The deployment stop: the one reader and the one writer of the record that
//! halts deployments of a runtime. Engaged halts; absent or clear admits. Who
//! engaged it and why are on the record, and nothing is signed, issued, bound
//! to a release or consumed: the brake is a stop, not a per-deploy gate (ruling
//! brake-is-a-stop). The lifecycle brake is a different record and a different
//! store; nothing here touches it.
//!
//! The reader fails closed. A store that is present but foreign, ambiguous,
//! noncanonical, for another runtime, a symlink, or owned by anyone but the
//! expected uid reads as `Unreadable`, which does not admit.
use std::path::Path;
use anyhow::{Context, Result, anyhow, bail, ensure};
use cultcache_rs::{CultCacheEnvelope, DatabaseEntry, SingleFileMessagePackBackingStore};
use serde::{Deserialize, Serialize};
pub(crate) const DEPLOYMENT_STOP_SCHEMA: &str = "idunn.deployment_stop.v1";
pub(crate) const DEPLOYMENT_STOP_KEY: &str = "deployment-stop";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum StopState {
Engaged,
Clear,
}
impl StopState {
pub(crate) fn name(self) -> &'static str {
match self {
Self::Engaged => "engaged",
Self::Clear => "clear",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "idunn.deployment_stop", schema = "idunn.deployment_stop.v1")]
pub(crate) struct DeploymentStop {
#[cultcache(key = 0)]
pub(crate) schema_version: String,
#[cultcache(key = 1)]
pub(crate) runtime_id: String,
#[cultcache(key = 2)]
pub(crate) state: StopState,
/// The login name of the user who ran the verb, read from the process uid.
#[cultcache(key = 3)]
pub(crate) by: String,
#[cultcache(key = 4)]
pub(crate) reason: String,
/// The verb's clock.
#[cultcache(key = 5)]
pub(crate) at_unix_millis: u64,
/// The previous record's sequence plus one; 1 for the first.
#[cultcache(key = 6)]
pub(crate) sequence: u64,
}
fn bounded(value: &str, label: &str, max: usize) -> Result<()> {
ensure!(
!value.trim().is_empty() && value.len() <= max && !value.chars().any(char::is_control),
"deployment stop {label} is empty, oversized, or contains control characters"
);
Ok(())
}
impl DeploymentStop {
pub(crate) fn validate(&self) -> Result<()> {
ensure!(
self.schema_version == DEPLOYMENT_STOP_SCHEMA,
"deployment stop schema is unsupported"
);
bounded(&self.runtime_id, "runtime id", 256)?;
bounded(&self.by, "author", 256)?;
bounded(&self.reason, "reason", 1024)?;
ensure!(
self.at_unix_millis > 0 && self.sequence > 0,
"deployment stop time or sequence is zero"
);
Ok(())
}
fn envelope(&self) -> Result<CultCacheEnvelope> {
self.validate()?;
let stored_at = chrono::DateTime::from_timestamp_millis(i64::try_from(self.at_unix_millis)?)
.ok_or_else(|| anyhow!("deployment stop time is out of range"))?
.to_rfc3339();
Ok(CultCacheEnvelope {
key: DEPLOYMENT_STOP_KEY.into(),
r#type: Self::TYPE.into(),
payload: rmp_serde::to_vec(self)?,
stored_at,
schema_id: Some(DEPLOYMENT_STOP_SCHEMA.into()),
})
}
fn decode(envelope: &CultCacheEnvelope) -> Result<Self> {
ensure!(
envelope.r#type == Self::TYPE
&& envelope.key == DEPLOYMENT_STOP_KEY
&& envelope.schema_id.as_deref() == Some(DEPLOYMENT_STOP_SCHEMA),
"deployment stop store holds a foreign record"
);
let record: Self = rmp_serde::from_slice(&envelope.payload)?;
ensure!(
rmp_serde::to_vec(&record)? == envelope.payload,
"deployment stop is noncanonical"
);
record.validate()?;
Ok(record)
}
}
/// What one store says about a runtime's deployments.
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Stop {
/// No store, or an empty one: nothing halts.
Absent,
Clear(DeploymentStop),
Engaged(DeploymentStop),
/// Present but not a record this runtime can trust. Does not admit.
Unreadable(String),
}
impl Stop {
pub(crate) fn admits(&self) -> bool {
matches!(self, Self::Absent | Self::Clear(_))
}
}
#[cfg(unix)]
pub(crate) fn process_uid() -> u32 {
// SAFETY: geteuid has no failure mode and no arguments.
unsafe { libc::geteuid() }
}
#[cfg(not(unix))]
pub(crate) fn process_uid() -> u32 {
0
}
#[cfg(unix)]
fn login_name(uid: u32) -> Result<String> {
// SAFETY: getpwuid returns null or a pointer into libc's static buffer,
// copied before any other call; the verbs are single threaded.
unsafe {
let entry = libc::getpwuid(uid);
ensure!(!entry.is_null(), "uid {uid} has no login name");
Ok(std::ffi::CStr::from_ptr((*entry).pw_name)
.to_string_lossy()
.into_owned())
}
}
#[cfg(not(unix))]
fn login_name(_: u32) -> Result<String> {
std::env::var("USERNAME").context("USERNAME is unset")
}
/// Open without following a final symlink and judge the file by fstat before
/// anything is read from it.
#[cfg(unix)]
fn check_file(path: &Path, expected_owner_uid: u32) -> Result<()> {
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
.with_context(|| format!("opening deployment stop store {}", path.display()))?;
let metadata = file.metadata()?;
ensure!(
metadata.is_file(),
"deployment stop store is not a regular file"
);
ensure!(
metadata.uid() == expected_owner_uid,
"deployment stop store is owned by uid {}, not {expected_owner_uid}",
metadata.uid()
);
Ok(())
}
#[cfg(not(unix))]
fn check_file(path: &Path, _: u32) -> Result<()> {
ensure!(
std::fs::metadata(path)?.is_file(),
"deployment stop store is not a regular file"
);
Ok(())
}
/// Absent means exactly this: the final component does not exist and its
/// parent is an existing directory. Every other failure to look (permission,
/// a parent that is a file, a symlink loop) is a store this runtime cannot
/// judge, and fails closed.
fn is_absent(path: &Path) -> Result<bool> {
match std::fs::symlink_metadata(path) {
Ok(_) => Ok(false),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
let parent = path.parent().filter(|p| !p.as_os_str().is_empty());
let parent = parent.unwrap_or_else(|| Path::new("."));
ensure!(
std::fs::metadata(parent).is_ok_and(|m| m.is_dir()),
"deployment stop store directory {} does not exist",
parent.display()
);
Ok(true)
}
Err(error) => Err(error)
.with_context(|| format!("looking for deployment stop store {}", path.display())),
}
}
fn read_checked(path: &Path, runtime_id: &str, owner: u32) -> Result<Option<DeploymentStop>> {
if is_absent(path)? {
return Ok(None);
}
check_file(path, owner)?;
ensure!(
std::fs::metadata(path)?.len() > 0,
"deployment stop store is zero length"
);
let entries = SingleFileMessagePackBackingStore::new(path).pull_all_read_only_snapshot()?;
match entries.as_slice() {
[] => bail!("deployment stop store holds no record"),
[entry] => {
let record = DeploymentStop::decode(entry)?;
ensure!(
record.runtime_id == runtime_id,
"deployment stop belongs to runtime {}",
record.runtime_id
);
Ok(Some(record))
}
_ => bail!("deployment stop store is ambiguous"),
}
}
pub(crate) fn read_stop(path: &Path, runtime_id: &str, expected_owner_uid: u32) -> Stop {
match read_checked(path, runtime_id, expected_owner_uid) {
Ok(None) => Stop::Absent,
Ok(Some(record)) if record.state == StopState::Clear => Stop::Clear(record),
Ok(Some(record)) => Stop::Engaged(record),
Err(error) => Stop::Unreadable(format!(
"{error:#}; to replace it run: idunn-provision deployment-brake-clear \
--store {} --runtime-id {runtime_id} --reason <text> (or deployment-brake-engage)",
path.display()
)),
}
}
/// Write the next record under a compare-exchange of the whole store, so a
/// concurrent verb or a store that moved since it was read is refused. The
/// author is the login name of the process uid and the sequence is the
/// predecessor's plus one (1 when there is none or it cannot be decoded);
/// neither is an argument.
///
/// This is also the operator's break-glass. A predecessor that is a retired v1
/// brake, another runtime's record, ambiguous, or zero length is replaced. A
/// file that is not a CultCache store at all (junk bytes) is replaced only
/// when it is a regular file owned by the caller, by removing it first. A
/// symlink or another user's file is refused. The sequence is not validated
/// beyond being positive: nothing reads it for authority.
pub(crate) fn replace_stop(
path: &Path,
runtime_id: &str,
state: StopState,
reason: &str,
now_unix_millis: u64,
) -> Result<DeploymentStop> {
let store = SingleFileMessagePackBackingStore::new(path);
let before = match store.pull_all_read_only_snapshot() {
Ok(entries) => entries,
Err(undecodable) => {
check_file(path, process_uid()).with_context(|| {
format!("deployment stop store is undecodable ({undecodable:#}) and not the caller's to replace")
})?;
std::fs::remove_file(path)?;
Vec::new()
}
};
let sequence = match before.as_slice() {
[entry] => match DeploymentStop::decode(entry) {
Ok(previous) => previous.sequence.saturating_add(1),
Err(_) => 1,
},
_ => 1,
};
let record = DeploymentStop {
schema_version: DEPLOYMENT_STOP_SCHEMA.into(),
runtime_id: runtime_id.into(),
state,
by: login_name(process_uid())?,
reason: reason.into(),
at_unix_millis: now_unix_millis,
sequence,
};
let next = record.envelope()?;
ensure!(
store.compare_exchange_snapshot(&before, &[next])?,
"deployment stop changed during transition"
);
Ok(record)
}
#[cfg(test)]
mod tests {
use super::*;
fn engage(path: &Path, reason: &str, at: u64) -> Result<DeploymentStop> {
replace_stop(path, "runtime", StopState::Engaged, reason, at)
}
#[test]
fn absent_and_empty_stores_admit_a_clear_one_admits_an_engaged_one_does_not() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("stop.cc");
let me = process_uid();
assert_eq!(read_stop(&path, "runtime", me), Stop::Absent);
engage(&path, "freeze for the audit", 100)?;
let Stop::Engaged(held) = read_stop(&path, "runtime", me) else {
bail!("an engaged stop did not read as engaged")
};
assert_eq!(held.reason, "freeze for the audit");
assert_eq!(held.by, login_name(me)?);
assert!(!Stop::Engaged(held).admits());
replace_stop(&path, "runtime", StopState::Clear, "audit done", 200)?;
let cleared = read_stop(&path, "runtime", me);
assert!(matches!(&cleared, Stop::Clear(record) if record.sequence == 2));
assert!(cleared.admits());
// Clear is not consumed: the same record admits again.
assert!(read_stop(&path, "runtime", me).admits());
Ok(())
}
#[test]
fn corrupt_foreign_other_runtime_symlinked_or_foreign_owned_stores_stop() -> Result<()> {
let dir = tempfile::tempdir()?;
let me = process_uid();
let path = dir.path().join("stop.cc");
engage(&path, "held", 100)?;
// Another runtime's record.
assert!(!read_stop(&path, "other", me).admits());
// Owned by someone other than the expected uid: even a CLEAR record
// from a foreign owner must not admit.
let cleared = dir.path().join("cleared.cc");
replace_stop(&cleared, "runtime", StopState::Clear, "thaw", 100)?;
assert!(read_stop(&cleared, "runtime", me).admits());
assert!(matches!(read_stop(&cleared, "runtime", me + 1), Stop::Unreadable(_)));
// A symlink to a valid, clear store.
let real = dir.path().join("real.cc");
replace_stop(&real, "runtime", StopState::Clear, "clear", 100)?;
let link = dir.path().join("link.cc");
#[cfg(unix)]
std::os::unix::fs::symlink(&real, &link)?;
#[cfg(unix)]
assert!(matches!(read_stop(&link, "runtime", me), Stop::Unreadable(_)));
// Bytes that are no store at all.
let junk = dir.path().join("junk.cc");
std::fs::write(&junk, b"not msgpack at all")?;
assert!(matches!(read_stop(&junk, "runtime", me), Stop::Unreadable(_)));
// A record of another type under another key (the retired v1 brake).
let foreign = dir.path().join("foreign.cc");
let store = SingleFileMessagePackBackingStore::new(&foreign);
let envelope = CultCacheEnvelope {
key: "deployment".into(),
r#type: "idunn.deployment_brake".into(),
payload: vec![0x90],
stored_at: "1970-01-01T00:00:00+00:00".into(),
schema_id: Some("idunn.deployment_brake.v1".into()),
};
assert!(store.compare_exchange_snapshot(&[], &[envelope])?);
assert!(matches!(read_stop(&foreign, "runtime", me), Stop::Unreadable(_)));
// A noncanonical payload under the right identity.
let mut stop = DeploymentStop::decode(&store_entry(&path)?)?;
stop.sequence = 9;
let mut loose = stop.envelope()?;
loose.payload.push(0xc0);
let noncanonical = dir.path().join("noncanonical.cc");
assert!(SingleFileMessagePackBackingStore::new(&noncanonical)
.compare_exchange_snapshot(&[], &[loose])?);
assert!(matches!(read_stop(&noncanonical, "runtime", me), Stop::Unreadable(_)));
Ok(())
}
fn store_entry(path: &Path) -> Result<CultCacheEnvelope> {
let mut entries = SingleFileMessagePackBackingStore::new(path).pull_all_read_only_snapshot()?;
entries.pop().context("no entry")
}
#[test]
fn a_verb_replaces_a_retired_brake_record_and_the_sequence_advances_by_one() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("stop.cc");
let store = SingleFileMessagePackBackingStore::new(&path);
let retired = CultCacheEnvelope {
key: "deployment".into(),
r#type: "idunn.deployment_brake".into(),
payload: vec![0x90],
stored_at: "1970-01-01T00:00:00+00:00".into(),
schema_id: Some("idunn.deployment_brake.v1".into()),
};
assert!(store.compare_exchange_snapshot(&[], &[retired])?);
assert_eq!(engage(&path, "first", 10)?.sequence, 1);
assert_eq!(store.pull_all_read_only_snapshot()?.len(), 1);
assert_eq!(engage(&path, "second", 11)?.sequence, 2);
assert_eq!(
replace_stop(&path, "runtime", StopState::Clear, "third", 12)?.sequence,
3
);
assert_eq!(DeploymentStop::decode(&store_entry(&path)?)?.reason, "third");
Ok(())
}
#[test]
fn a_stale_exchange_is_refused_and_a_record_without_a_reason_is_not_written() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("stop.cc");
engage(&path, "held", 10)?;
let store = SingleFileMessagePackBackingStore::new(&path);
let stale = store.pull_all_read_only_snapshot()?;
engage(&path, "moved", 11)?;
let next = DeploymentStop {
schema_version: DEPLOYMENT_STOP_SCHEMA.into(),
runtime_id: "runtime".into(),
state: StopState::Clear,
by: "someone".into(),
reason: "late".into(),
at_unix_millis: 12,
sequence: 2,
}
.envelope()?;
assert!(!store.compare_exchange_snapshot(&stale, &[next])?);
assert!(replace_stop(&path, "runtime", StopState::Clear, " ", 13).is_err());
assert_eq!(DeploymentStop::decode(&store_entry(&path)?)?.reason, "moved");
Ok(())
}
#[test]
fn only_a_missing_file_in_an_existing_directory_is_absent() -> Result<()> {
let dir = tempfile::tempdir()?;
let me = process_uid();
// Parent is a regular file: ENOTDIR, not absence.
let file = dir.path().join("plain");
std::fs::write(&file, b"x")?;
assert!(matches!(read_stop(&file.join("stop.cc"), "runtime", me), Stop::Unreadable(_)));
// Parent does not exist.
assert!(matches!(
read_stop(&dir.path().join("nope/stop.cc"), "runtime", me),
Stop::Unreadable(_)
));
// Zero-length file.
let empty = dir.path().join("empty.cc");
std::fs::write(&empty, b"")?;
assert!(matches!(read_stop(&empty, "runtime", me), Stop::Unreadable(_)));
// An existing directory with no store is absent.
assert_eq!(read_stop(&dir.path().join("stop.cc"), "runtime", me), Stop::Absent);
// A parent that cannot be searched (skipped as root, which can).
#[cfg(unix)]
if me != 0 {
use std::os::unix::fs::PermissionsExt;
let locked = dir.path().join("locked");
std::fs::create_dir(&locked)?;
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000))?;
let reading = read_stop(&locked.join("stop.cc"), "runtime", me);
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o700))?;
assert!(matches!(reading, Stop::Unreadable(_)), "{reading:?}");
}
Ok(())
}
#[test]
fn the_author_is_the_uid_login_never_the_environment() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("stop.cc");
// SAFETY: no other test reads USER, LOGNAME or LNAME.
unsafe {
std::env::set_var("USER", "spoof");
std::env::set_var("LOGNAME", "spoof");
}
let written = engage(&path, "held", 10)?;
assert_eq!(written.by, login_name(process_uid())?);
assert_ne!(written.by, "spoof");
Ok(())
}
#[test]
fn unreadable_names_the_command_and_a_break_glass_clear_replaces_junk_and_v1() -> Result<()> {
let dir = tempfile::tempdir()?;
let me = process_uid();
let path = dir.path().join("stop.cc");
std::fs::write(&path, b"junk that is no store")?;
let Stop::Unreadable(why) = read_stop(&path, "runtime", me) else {
bail!("junk admitted")
};
assert!(why.contains("idunn-provision deployment-brake-clear --store"), "{why}");
assert!(why.contains("--runtime-id runtime"), "{why}");
let cleared = replace_stop(&path, "runtime", StopState::Clear, "operator break-glass", 50)?;
assert_eq!(cleared.sequence, 1);
assert!(read_stop(&path, "runtime", me).admits());
// A symlink is not the caller's to replace.
let real = dir.path().join("real.cc");
std::fs::write(&real, b"junk")?;
let link = dir.path().join("link.cc");
#[cfg(unix)]
{
std::os::unix::fs::symlink(&real, &link)?;
assert!(replace_stop(&link, "runtime", StopState::Clear, "x", 60).is_err());
assert_eq!(std::fs::read(&real)?, b"junk");
}
Ok(())
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
mod brake;
pub mod control_plane;
pub mod deployment;
pub mod deployment_plan;

View file

@ -1,4 +1,5 @@
use anyhow::{Context, Result, anyhow, bail};
use crate::brake::{Stop, StopState, process_uid, read_stop, replace_stop};
use anyhow::{Context, Result, anyhow, bail, ensure};
use cultcache_rs::{
CultCacheEnvelope, CultCacheExpectedEnvelope, DatabaseEntry, SingleFileMessagePackBackingStore,
};
@ -6,15 +7,13 @@ use cultnet_rs::{
GAMECULT_SERVICE_TRUST_ANCHOR_SCHEMA, GameCultProviderHealthIdentity,
GameCultServiceTrustAnchorRecord, IDUNN_AUTHENTICATED_PROVIDER_HEALTH_PROJECTION_SCHEMA,
IDUNN_AUTHENTICATED_PROVIDER_HEALTH_PROJECTION_SIGNING_PURPOSE,
IDUNN_DAEMON_HEALTH_TRUST_BINDING_SCHEMA, IDUNN_DEPLOYMENT_BRAKE_AUTHORITY,
IDUNN_DEPLOYMENT_BRAKE_ID, IDUNN_DEPLOYMENT_BRAKE_SCHEMA, IDUNN_DEPLOYMENT_BRAKE_SCOPE,
IDUNN_DEPLOYMENT_RELEASE_PURPOSE, IDUNN_LIFECYCLE_BRAKE_AUTHORITY,
IDUNN_DAEMON_HEALTH_TRUST_BINDING_SCHEMA,
IDUNN_LIFECYCLE_BRAKE_AUTHORITY,
IDUNN_LIFECYCLE_BRAKE_SCHEMA, IDUNN_LIFECYCLE_BRAKE_SCOPE, IdunnDaemonHealthTrustBindingRecord,
IdunnDeploymentBrakeObservation, IdunnDeploymentBrakeOperatorIdentity,
IdunnDeploymentBrakeRecord, IdunnDeploymentBrakeReleasePurpose, IdunnLifecycleBrakeObservation,
IdunnLifecycleBrakeObservation,
IdunnLifecycleBrakeRecord, IdunnServiceIdentity, OdinTopologyIdentity, ServiceIdentityProfile,
ServiceIdentityTrustAnchor, derive_service_identity_id, enroll_service_identity_at,
evaluate_idunn_continuity_restart, evaluate_idunn_deployment_brake,
evaluate_idunn_continuity_restart,
export_service_identity_trust_anchor, open_service_identity_at,
};
use std::collections::{BTreeMap, BTreeSet};
@ -80,17 +79,8 @@ pub fn run(args: impl IntoIterator<Item = String>) -> Result<()> {
"export-odin-topology-public-anchor" => {
export_identity_anchor::<OdinTopologyIdentity>(&options, "Odin topology public anchor")?
}
"enroll-deployment-brake-operator" => {
enroll_identity::<IdunnDeploymentBrakeOperatorIdentity>(&options)?
}
"export-deployment-brake-operator-anchor" => {
export_identity_anchor::<IdunnDeploymentBrakeOperatorIdentity>(
&options,
"deployment brake operator anchor",
)?
}
"deployment-brake-engage" => deployment_brake_engage(&options)?,
"deployment-brake-release" => deployment_brake_release(&options)?,
"deployment-brake-engage" => deployment_brake_write(&options, StopState::Engaged)?,
"deployment-brake-clear" => deployment_brake_write(&options, StopState::Clear)?,
"deployment-brake-status" => deployment_brake_status(&options)?,
"lifecycle-brake-engage" => lifecycle_brake_engage(&options)?,
"lifecycle-brake-release" => lifecycle_brake_release(&options)?,
@ -157,153 +147,58 @@ fn provider_health_public_key(options: &BTreeMap<String, String>) -> Result<Stri
encode_public_key(&signer.entry().public_key)
}
fn deployment_brake_engage(options: &BTreeMap<String, String>) -> Result<()> {
require_only(
options,
&[
"store",
"runtime-id",
"owner",
"reason",
"observed-at-unix-millis",
],
/// Engage or clear the deployment stop. The author is the process uid's login
/// name and the time is this verb's clock: neither is an argument.
fn deployment_brake_write(options: &BTreeMap<String, String>, state: StopState) -> Result<()> {
require_only(options, &["store", "runtime-id", "reason"])?;
let now = u64::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_millis(),
)?;
let observed = parse_u64(options, "observed-at-unix-millis")?;
let record = IdunnDeploymentBrakeRecord {
schema_version: IDUNN_DEPLOYMENT_BRAKE_SCHEMA.into(),
brake_id: IDUNN_DEPLOYMENT_BRAKE_ID.into(),
authority: IDUNN_DEPLOYMENT_BRAKE_AUTHORITY.into(),
runtime_id: required(options, "runtime-id")?.into(),
status: "engaged".into(),
scope: IDUNN_DEPLOYMENT_BRAKE_SCOPE.into(),
reason: required(options, "reason")?.into(),
observed_at_unix_millis: observed,
expires_at_unix_millis: None,
authorization_id: None,
authorization_purpose: None,
authorized_release_id: None,
authorized_deployment_id: None,
authorized_by: None,
authorization_issued_at_unix_millis: None,
authorization_expires_at_unix_millis: None,
signature_algorithm: None,
signature: None,
private_state_exposed: false,
updated_by: required(options, "owner")?.into(),
};
record.validate()?;
replace_brake(&path(options, "store")?, record, observed)
}
fn deployment_brake_release(options: &BTreeMap<String, String>) -> Result<()> {
require_only(
options,
&[
"store",
"private-store",
"runtime-id",
"owner",
"reason",
"authorization-id",
"release-id",
"deployment-id",
"issued-at-unix-millis",
"expires-at-unix-millis",
],
)?;
let issued = parse_u64(options, "issued-at-unix-millis")?;
let expires = parse_u64(options, "expires-at-unix-millis")?;
let signer = open_service_identity_at::<IdunnDeploymentBrakeOperatorIdentity>(&path(
options,
"private-store",
)?)?;
let mut record = IdunnDeploymentBrakeRecord {
schema_version: IDUNN_DEPLOYMENT_BRAKE_SCHEMA.into(),
brake_id: IDUNN_DEPLOYMENT_BRAKE_ID.into(),
authority: IDUNN_DEPLOYMENT_BRAKE_AUTHORITY.into(),
runtime_id: required(options, "runtime-id")?.into(),
status: "released".into(),
scope: IDUNN_DEPLOYMENT_BRAKE_SCOPE.into(),
reason: required(options, "reason")?.into(),
observed_at_unix_millis: issued,
expires_at_unix_millis: Some(expires),
authorization_id: Some(required(options, "authorization-id")?.into()),
authorization_purpose: Some(IDUNN_DEPLOYMENT_RELEASE_PURPOSE.into()),
authorized_release_id: Some(required(options, "release-id")?.into()),
authorized_deployment_id: Some(required(options, "deployment-id")?.into()),
authorized_by: Some(signer.trust_anchor()?.identity_id),
authorization_issued_at_unix_millis: Some(issued),
authorization_expires_at_unix_millis: Some(expires),
signature_algorithm: Some("ed25519".into()),
signature: None,
private_state_exposed: false,
updated_by: required(options, "owner")?.into(),
};
record.signature = Some(
signer
.sign::<IdunnDeploymentBrakeReleasePurpose>(&rmp_serde::to_vec(&record)?)
.signature,
);
record.validate()?;
replace_brake(&path(options, "store")?, record, issued)
}
fn deployment_brake_status(options: &BTreeMap<String, String>) -> Result<()> {
allow_only(
options,
&[
"store",
"operator-anchor",
"runtime-id",
"release-id",
"deployment-id",
"now-unix-millis",
],
)?;
let exact_actuation_fields = ["release-id", "deployment-id", "now-unix-millis"];
let exact_actuation_field_count = exact_actuation_fields
.into_iter()
.filter(|field| options.contains_key(*field))
.count();
if exact_actuation_field_count != 0
&& exact_actuation_field_count != exact_actuation_fields.len()
{
bail!("exact deployment-brake actuation validation requires release, deployment, and time");
}
let record = read_brake(&path(options, "store")?)?;
if record.runtime_id != required(options, "runtime-id")? {
bail!("deployment brake belongs to another runtime")
}
if record.status == "engaged" {
if exact_actuation_field_count != 0 {
bail!("engaged deployment brake denies exact actuation validation");
}
println!(
"engaged runtime={} scope={} owner={} reason={}",
record.runtime_id, record.scope, record.updated_by, record.reason
);
return Ok(());
}
let anchor = read_operator_anchor(&path(options, "operator-anchor")?)?;
evaluate_idunn_deployment_brake(
IdunnDeploymentBrakeObservation::Present(&record),
&anchor,
let record = replace_stop(
&path(options, "store")?,
required(options, "runtime-id")?,
required(options, "release-id")?,
required(options, "deployment-id")?,
parse_u64(options, "now-unix-millis")?,
)
.map_err(|d| anyhow!("deployment brake denied actuation: {d:?}"))?;
state,
required(options, "reason")?,
now,
)?;
println!(
"released runtime={} release={} deployment={} owner={}",
"{} runtime={} by={} sequence={}",
record.state.name(),
record.runtime_id,
record.authorized_release_id.unwrap(),
record.authorized_deployment_id.unwrap(),
record.updated_by
record.by,
record.sequence
);
Ok(())
}
/// Print the stop and exit nonzero unless deployments admit, so a script can
/// gate on it as it could on the retired exact-actuation validation.
fn deployment_brake_status(options: &BTreeMap<String, String>) -> Result<()> {
require_only(options, &["store", "runtime-id"])?;
let stop = read_stop(
&path(options, "store")?,
required(options, "runtime-id")?,
process_uid(),
);
match &stop {
Stop::Absent => println!("absent: deployments admit"),
Stop::Unreadable(why) => println!("unreadable: deployments wait ({why})"),
Stop::Clear(record) | Stop::Engaged(record) => println!(
"{} runtime={} by={} at={} sequence={} reason={}",
record.state.name(),
record.runtime_id,
record.by,
record.at_unix_millis,
record.sequence,
record.reason
),
}
ensure!(stop.admits(), "the deployment brake does not admit deployments");
Ok(())
}
fn lifecycle_brake_engage(options: &BTreeMap<String, String>) -> Result<()> {
require_only(
options,
@ -482,70 +377,6 @@ fn lifecycle_identifier<'a>(options: &'a BTreeMap<String, String>, name: &str) -
Ok(value)
}
fn replace_brake(path: &Path, record: IdunnDeploymentBrakeRecord, millis: u64) -> Result<()> {
let store = SingleFileMessagePackBackingStore::new(path);
let entries = store.pull_all_read_only_snapshot()?;
let current = match entries.as_slice() {
[] => None,
[entry]
if entry.r#type == IdunnDeploymentBrakeRecord::TYPE
&& entry.key == IDUNN_DEPLOYMENT_BRAKE_ID =>
{
Some(entry.clone())
}
_ => bail!("deployment brake store is corrupt or ambiguous"),
};
let next = typed_envelope(
IDUNN_DEPLOYMENT_BRAKE_ID,
&record,
IDUNN_DEPLOYMENT_BRAKE_SCHEMA,
millis,
)?;
if !store.compare_exchange(
&[CultCacheExpectedEnvelope {
r#type: IdunnDeploymentBrakeRecord::TYPE.into(),
key: IDUNN_DEPLOYMENT_BRAKE_ID.into(),
current,
}],
&[next],
)? {
bail!("deployment brake changed during transition");
}
Ok(())
}
fn read_brake(path: &Path) -> Result<IdunnDeploymentBrakeRecord> {
let entries = SingleFileMessagePackBackingStore::new(path).pull_all_read_only_snapshot()?;
let [entry] = entries.as_slice() else {
bail!("deployment brake is missing or ambiguous")
};
if entry.r#type != IdunnDeploymentBrakeRecord::TYPE
|| entry.key != IDUNN_DEPLOYMENT_BRAKE_ID
|| entry.schema_id.as_deref() != Some(IDUNN_DEPLOYMENT_BRAKE_SCHEMA)
{
bail!("deployment brake is foreign")
}
let record: IdunnDeploymentBrakeRecord = rmp_serde::from_slice(&entry.payload)?;
if rmp_serde::to_vec(&record)? != entry.payload {
bail!("deployment brake is noncanonical")
}
record.validate()?;
Ok(record)
}
fn read_operator_anchor(path: &Path) -> Result<ServiceIdentityTrustAnchor> {
let entries = SingleFileMessagePackBackingStore::new(path).pull_all_read_only_snapshot()?;
let [entry] = entries.as_slice() else {
bail!("operator anchor is missing or ambiguous")
};
if entry.r#type != IdunnDeploymentBrakeOperatorIdentity::TRUST_ANCHOR_TYPE
|| entry.key != IdunnDeploymentBrakeOperatorIdentity::TRUST_ANCHOR_KEY
{
bail!("operator anchor is foreign")
}
Ok(rmp_serde::from_slice(&entry.payload)?)
}
fn validate_health_binding_store(path: &Path) -> Result<()> {
let entries = SingleFileMessagePackBackingStore::new(path).pull_all_read_only_snapshot()?;
validated_health_bindings(&entries)?;
@ -1127,7 +958,7 @@ fn normalized(path: &Path) -> Result<PathBuf> {
}
fn usage() -> &'static str {
"Usage: idunn-provision enroll-provider-health-identity --private-store <path>\n idunn-provision provider-health-public-key --private-store <path>\n idunn-provision export-provider-health-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision enroll-idunn-identity --private-store <path>\n idunn-provision export-idunn-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision enroll-odin-topology-identity --private-store <path>\n idunn-provision export-odin-topology-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision enroll-deployment-brake-operator --private-store <path>\n idunn-provision export-deployment-brake-operator-anchor --private-store <path> --public-anchor <path>\n idunn-provision deployment-brake-engage --store <path> --runtime-id <id> --owner <principal> --reason <text> --observed-at-unix-millis <u64>\n idunn-provision deployment-brake-release --store <path> --private-store <path> --runtime-id <id> --owner <principal> --reason <text> --authorization-id <id> --release-id <id> --deployment-id <id> --issued-at-unix-millis <u64> --expires-at-unix-millis <u64>\n idunn-provision deployment-brake-status --store <path> --operator-anchor <path> --runtime-id <id> [--release-id <id> --deployment-id <id> --now-unix-millis <u64>]\n idunn-provision lifecycle-brake-engage --store <path> --runtime-id <id> --target <id> --reason <text> --updated-at-unix-millis <u64>\n idunn-provision lifecycle-brake-release --store <path> --runtime-id <id> --target <id> --reason <text> --updated-at-unix-millis <u64> [--released-until-unix-millis <u64>]\n idunn-provision lifecycle-brake-status --store <path> --runtime-id <id> --target <id> --now-unix-millis <u64>\n idunn-provision create-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision add-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision require-daemon-health-release-binding --store <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex>\n idunn-provision rotate-daemon-health-trust-signer --output <path> --binding-id <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64>\n idunn-provision validate-daemon-health-trust-binding --input <path>\n idunn-provision create-provider-projection-trust-anchor --output <path> --trust-anchor-id <id> --runtime-id <id> --idunn-public-anchor <path> --bound-at-unix-millis <u64> --expires-at-unix-millis <u64>\n idunn-provision validate-provider-projection-trust-anchor --input <path> --idunn-public-anchor <path>"
"Usage: idunn-provision enroll-provider-health-identity --private-store <path>\n idunn-provision provider-health-public-key --private-store <path>\n idunn-provision export-provider-health-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision enroll-idunn-identity --private-store <path>\n idunn-provision export-idunn-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision enroll-odin-topology-identity --private-store <path>\n idunn-provision export-odin-topology-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision deployment-brake-engage --store <path> --runtime-id <id> --reason <text>\n idunn-provision deployment-brake-clear --store <path> --runtime-id <id> --reason <text>\n idunn-provision deployment-brake-status --store <path> --runtime-id <id>\n idunn-provision lifecycle-brake-engage --store <path> --runtime-id <id> --target <id> --reason <text> --updated-at-unix-millis <u64>\n idunn-provision lifecycle-brake-release --store <path> --runtime-id <id> --target <id> --reason <text> --updated-at-unix-millis <u64> [--released-until-unix-millis <u64>]\n idunn-provision lifecycle-brake-status --store <path> --runtime-id <id> --target <id> --now-unix-millis <u64>\n idunn-provision create-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision add-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision require-daemon-health-release-binding --store <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex>\n idunn-provision rotate-daemon-health-trust-signer --output <path> --binding-id <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64>\n idunn-provision validate-daemon-health-trust-binding --input <path>\n idunn-provision create-provider-projection-trust-anchor --output <path> --trust-anchor-id <id> --runtime-id <id> --idunn-public-anchor <path> --bound-at-unix-millis <u64> --expires-at-unix-millis <u64>\n idunn-provision validate-provider-projection-trust-anchor --input <path> --idunn-public-anchor <path>"
}
#[cfg(test)]
@ -2157,120 +1988,74 @@ mod tests {
}
#[test]
fn brake_operator_can_issue_one_exact_bounded_rollout_grant() -> Result<()> {
fn provision_engage_clear_sequence() -> Result<()> {
let temp = TempDir::new()?;
let private = temp.path().join("operator-private.cc");
let anchor = temp.path().join("operator-public.cc");
let store = temp.path().join("brake.cc");
invoke(&[
"enroll-deployment-brake-operator",
"--private-store",
private.to_str().unwrap(),
])?;
invoke(&[
"export-deployment-brake-operator-anchor",
"--private-store",
private.to_str().unwrap(),
"--public-anchor",
anchor.to_str().unwrap(),
])?;
invoke(&[
"deployment-brake-engage",
"--store",
store.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
"--owner",
"operator/metacrat",
"--reason",
"sleep mode",
"--observed-at-unix-millis",
"100",
])?;
assert_eq!(read_brake(&store)?.status, "engaged");
let me = process_uid();
let engage = |verb: &str, reason: &str| {
invoke(&[
verb,
"--store",
store.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
"--reason",
reason,
])
};
assert_eq!(read_stop(&store, "yggdrasil", me), Stop::Absent);
invoke(&[
"deployment-brake-status",
"--store",
store.to_str().unwrap(),
"--operator-anchor",
anchor.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
])?;
assert!(
engage("deployment-brake-engage", "freeze")?;
let status = || {
invoke(&[
"deployment-brake-status",
"--store",
store.to_str().unwrap(),
"--operator-anchor",
anchor.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
"--release-id",
"commit-4",
"--deployment-id",
"request-4",
"--now-unix-millis",
"150",
])
.is_err()
);
invoke(&[
"deployment-brake-release",
"--store",
store.to_str().unwrap(),
"--private-store",
private.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
"--owner",
"operator/metacrat",
"--reason",
"one rollout",
"--authorization-id",
"auth/r4",
"--release-id",
"commit-4",
"--deployment-id",
"request-4",
"--issued-at-unix-millis",
"200",
"--expires-at-unix-millis",
"800",
])?;
invoke(&[
"deployment-brake-status",
"--store",
store.to_str().unwrap(),
"--operator-anchor",
anchor.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
"--release-id",
"commit-4",
"--deployment-id",
"request-4",
"--now-unix-millis",
"500",
])?;
assert!(
invoke(&[
"deployment-brake-status",
"--store",
store.to_str().unwrap(),
"--operator-anchor",
anchor.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
"--release-id",
"substituted",
"--deployment-id",
"request-4",
"--now-unix-millis",
"500"
])
.is_err()
);
};
assert!(status().is_err(), "an engaged stop must not pass status");
let Stop::Engaged(first) = read_stop(&store, "yggdrasil", me) else {
bail!("engage did not engage")
};
assert_eq!((first.sequence, first.reason.as_str()), (1, "freeze"));
assert!(!first.by.is_empty() && first.at_unix_millis > 0);
engage("deployment-brake-clear", "thawed")?;
status()?;
let Stop::Clear(second) = read_stop(&store, "yggdrasil", me) else {
bail!("clear did not clear")
};
assert_eq!((second.sequence, second.by), (2, first.by));
assert!(second.at_unix_millis >= first.at_unix_millis);
// The author and the time are not arguments, and the retired
// signing arguments are refused.
for stray in ["--owner", "--observed-at-unix-millis", "--private-store"] {
assert!(
invoke(&[
"deployment-brake-engage",
"--store",
store.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
"--reason",
"x",
stray,
"y",
])
.is_err(),
"{stray}"
);
}
for gone in ["deployment-brake-release", "enroll-deployment-brake-operator"] {
assert!(invoke(&[gone]).is_err(), "{gone}");
}
Ok(())
}
@ -2309,7 +2094,6 @@ mod tests {
assert_eq!(engaged.status, "engaged");
assert_eq!(engaged.runtime_id, "idunn-yggdrasil");
assert_eq!(engaged.target, "ghostlight");
assert!(read_brake(&store).is_err());
assert!(
invoke(&[
"lifecycle-brake-status",
@ -2426,57 +2210,4 @@ mod tests {
assert_eq!(std::fs::read(&store)?, released_bytes);
Ok(())
}
#[test]
fn brake_compare_exchange_rejects_stale_transition_snapshot() -> Result<()> {
let temp = TempDir::new()?;
let store_path = temp.path().join("brake.cc");
let base = IdunnDeploymentBrakeRecord {
schema_version: IDUNN_DEPLOYMENT_BRAKE_SCHEMA.into(),
brake_id: IDUNN_DEPLOYMENT_BRAKE_ID.into(),
authority: IDUNN_DEPLOYMENT_BRAKE_AUTHORITY.into(),
runtime_id: "yggdrasil".into(),
status: "engaged".into(),
scope: IDUNN_DEPLOYMENT_BRAKE_SCOPE.into(),
reason: "first".into(),
observed_at_unix_millis: 100,
expires_at_unix_millis: None,
authorization_id: None,
authorization_purpose: None,
authorized_release_id: None,
authorized_deployment_id: None,
authorized_by: None,
authorization_issued_at_unix_millis: None,
authorization_expires_at_unix_millis: None,
signature_algorithm: None,
signature: None,
private_state_exposed: false,
updated_by: "operator/a".into(),
};
replace_brake(&store_path, base.clone(), 100)?;
let stale = SingleFileMessagePackBackingStore::new(&store_path)
.pull_all_read_only_snapshot()?
.remove(0);
let mut changed = base.clone();
changed.reason = "racing writer".into();
changed.observed_at_unix_millis = 101;
replace_brake(&store_path, changed, 101)?;
let candidate = typed_envelope(
IDUNN_DEPLOYMENT_BRAKE_ID,
&base,
IDUNN_DEPLOYMENT_BRAKE_SCHEMA,
102,
)?;
assert!(
!SingleFileMessagePackBackingStore::new(&store_path).compare_exchange(
&[CultCacheExpectedEnvelope {
r#type: IdunnDeploymentBrakeRecord::TYPE.into(),
key: IDUNN_DEPLOYMENT_BRAKE_ID.into(),
current: Some(stale)
}],
&[candidate]
)?
);
Ok(())
}
}