Merge B3: route-proof readiness, held records, strict challenge reader
ReadinessClass::of is the one class authority; recipes declare readiness. Held records are never aborted by Idunn; cancel accepts a held pre-fence Deploy. Silent means no byte; any byte then a stop is Refused. Soul: merge with follow-ups, safe to install on Yggdrasil. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
d32395ac99
3 changed files with 4380 additions and 331 deletions
3268
src/control_plane.rs
3268
src/control_plane.rs
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,7 @@ use cultnet_rs::{
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::control_plane::SequenceAdmittedReady;
|
||||
use crate::control_plane::{ODIN_RENDEZVOUS_CAPABILITY, ReadinessClass, SequenceAdmittedReady};
|
||||
use crate::deployment::{
|
||||
CapabilityDependency, DeadlineBinding, DependencyKind, ExternalCapabilityBinding, OperatorBinding,
|
||||
ServiceTransport, SourceSelectionPolicy, StartupOrder, StateDeclaration, TargetDeclaration,
|
||||
|
|
@ -640,6 +640,25 @@ impl CompiledDeploymentPlan {
|
|||
Ok((declaration, binding))
|
||||
}
|
||||
|
||||
/// How this plan's target proves readiness, from its recipe's declarations
|
||||
/// and its binding's route, or the typed refusal that it declares none.
|
||||
/// Admission asks this before anything is sealed or installed.
|
||||
pub(crate) fn readiness_class(&self) -> Result<ReadinessClass> {
|
||||
let (declaration, binding) = self.parsed_inputs()?;
|
||||
Ok(ReadinessClass::declared(
|
||||
&declaration.target,
|
||||
declaration
|
||||
.provides
|
||||
.iter()
|
||||
.any(|provided| provided.capability == ODIN_RENDEZVOUS_CAPABILITY),
|
||||
declaration.dependencies.iter().any(|dependency| {
|
||||
dependency.kind == DependencyKind::SharedInfrastructure
|
||||
&& dependency.capability == ODIN_RENDEZVOUS_CAPABILITY
|
||||
}),
|
||||
binding.route.is_some(),
|
||||
)?)
|
||||
}
|
||||
|
||||
fn recomputed_plan_id(&self) -> Result<String> {
|
||||
let mut unsigned = self.clone();
|
||||
unsigned.plan_id.clear();
|
||||
|
|
@ -1547,6 +1566,71 @@ nodes = ["yggdrasil"]
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plan_names_how_its_target_proves_readiness_and_agrees_with_its_expected() {
|
||||
use crate::control_plane::UndeclaredReadiness;
|
||||
let unrouted_binding = {
|
||||
let (head, tail) = BINDING.split_once("[route]").unwrap();
|
||||
format!("{head}[brakes]{}", tail.split_once("[brakes]").unwrap().1)
|
||||
};
|
||||
let unrouted = |recipe: &str| {
|
||||
recipe
|
||||
.replace("route_required = true", "route_required = false")
|
||||
.replace(
|
||||
r#"["GAMECULT_IDUNN_CANDIDATE_BIND", "GAMECULT_IDUNN_RUNTIME_BUNDLE"]"#,
|
||||
r#"["GAMECULT_IDUNN_RUNTIME_BUNDLE"]"#,
|
||||
)
|
||||
};
|
||||
let without_odin = RECIPE.split("[[dependencies]]").next().unwrap().to_owned();
|
||||
let provides_odin = |recipe: &str| {
|
||||
recipe.replace("capability = \"service.runtime\"", "capability = \"odin.verse-rendezvous\"")
|
||||
};
|
||||
let cases: [(&str, String, &str, Option<u16>, Result<ReadinessClass, ()>); 6] = [
|
||||
("depends on Odin, routed", RECIPE.to_owned(), BINDING, Some(18001), Ok(ReadinessClass::OdinCorrelated)),
|
||||
("routed, no Odin", without_odin.clone(), BINDING, Some(18001), Ok(ReadinessClass::RouteProof)),
|
||||
("unrouted, no Odin", unrouted(&without_odin), &unrouted_binding, None, Err(())),
|
||||
("provides Odin, unrouted", provides_odin(&unrouted(&without_odin)), &unrouted_binding, None, Ok(ReadinessClass::OdinSelf)),
|
||||
("depends on Odin, unrouted", unrouted(RECIPE), &unrouted_binding, None, Ok(ReadinessClass::OdinCorrelated)),
|
||||
// Only the shared-infrastructure declaration is the declaration.
|
||||
("requires the Odin capability, routed", RECIPE.replace("kind = \"shared-infrastructure\"", "kind = \"required\""), BINDING, Some(18001), Ok(ReadinessClass::RouteProof)),
|
||||
];
|
||||
for (name, recipe, binding, port, outcome) in cases {
|
||||
let providers = [ready_odin_provider("odin", "odin-yggdrasil", 1)];
|
||||
let plan = compile_deployment_plan(
|
||||
recipe.as_bytes(),
|
||||
binding.as_bytes(),
|
||||
source(&recipe),
|
||||
"service-incarnation-1",
|
||||
port,
|
||||
110,
|
||||
if recipe.contains("[[dependencies]]") { &providers } else { &[] },
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{name}: {error:#}"));
|
||||
let release = SealedRelease::new(
|
||||
&plan,
|
||||
vec![artifact_receipt()],
|
||||
vec![external_input_receipt()],
|
||||
120,
|
||||
)
|
||||
.unwrap();
|
||||
let expected = release.expected_projection(&plan).unwrap();
|
||||
match outcome {
|
||||
Ok(class) => {
|
||||
assert_eq!(plan.readiness_class().unwrap(), class, "{name}");
|
||||
assert_eq!(ReadinessClass::of(&expected), Ok(class), "{name}");
|
||||
}
|
||||
Err(()) => {
|
||||
let error = plan.readiness_class().expect_err(name);
|
||||
let undeclared = UndeclaredReadiness {
|
||||
target: "service".into(),
|
||||
};
|
||||
assert_eq!(error.downcast_ref::<UndeclaredReadiness>(), Some(&undeclared), "{name}");
|
||||
assert_eq!(ReadinessClass::of(&expected), Err(undeclared), "{name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expected_projection_is_sanitized_and_names_the_selected_graph() {
|
||||
let plan = plan();
|
||||
|
|
|
|||
1335
src/drivers.rs
1335
src/drivers.rs
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue