idunn-launch answers presence only: the splice, the unix child endpoint and --listener are deleted; the child binds the member bind and the probe reaches it over TCP
PresenceAcceptor serves one connection at a time within a 1 s deadline and never ends on an accept error; a dead presence thread fails the unit. Builds. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7805c0aa9a
commit
9173a0e8e6
5 changed files with 393 additions and 532 deletions
29
README.md
29
README.md
|
|
@ -154,8 +154,11 @@ An upstream daemon that does not speak Idunn's runtime contract is deployed
|
|||
with `idunn-launch` as its executable artifact and the upstream tree as a
|
||||
required adjacent artifact; the launcher finds the child inside the release that
|
||||
holds it. The launcher is the unit's process: it binds the
|
||||
candidate endpoint, answers Idunn's challenge, renders the child's config,
|
||||
starts a stateful child only once it holds the write lease, and signs presence.
|
||||
candidate endpoint and answers Idunn's challenge there, renders the child's
|
||||
config, starts a stateful child only once it holds the write lease, and signs
|
||||
presence. Members never pass through it: the binding's `[route.member]` makes
|
||||
Idunn route a second listener straight to the child, which binds the address
|
||||
Idunn gives it in `GAMECULT_IDUNN_MEMBER_BIND`.
|
||||
|
||||
```toml
|
||||
# recipe fragment — the upstream tree is built or fetched as an artifact
|
||||
|
|
@ -166,13 +169,31 @@ arguments = [
|
|||
{ kind = "literal", value = "upstream/bin/daemon" },
|
||||
{ kind = "literal", value = "--template" },
|
||||
{ kind = "literal", value = "config.tmpl" },
|
||||
{ kind = "literal", value = "--listener" },
|
||||
{ kind = "literal", value = "unix" },
|
||||
{ kind = "literal", value = "--health-path" },
|
||||
{ kind = "literal", value = "/health" },
|
||||
{ kind = "literal", value = "--health-contract" },
|
||||
{ kind = "literal", value = "example.presence-launcher.v1" },
|
||||
]
|
||||
required_environment = [
|
||||
"GAMECULT_IDUNN_RUNTIME_BUNDLE",
|
||||
"GAMECULT_IDUNN_CANDIDATE_BIND",
|
||||
"GAMECULT_IDUNN_MEMBER_BIND",
|
||||
]
|
||||
```
|
||||
|
||||
```toml
|
||||
# binding fragment — members connect to the member endpoint, which Idunn
|
||||
# forwards to the private port derived from the candidate: member
|
||||
# private_port_start + (candidate port - route private_port_start)
|
||||
[route]
|
||||
stable_endpoint = "tcp://10.77.0.1:8008"
|
||||
private_port_start = 18831
|
||||
private_port_end = 18839
|
||||
# ...
|
||||
|
||||
[route.member]
|
||||
stable_endpoint = "tcp://10.77.0.1:8448"
|
||||
private_port_start = 18841
|
||||
```
|
||||
|
||||
## Contracts
|
||||
|
|
|
|||
|
|
@ -83,24 +83,30 @@ activation key.
|
|||
Idunn owns admission, lease, route and continuity, as before.
|
||||
- **Inputs:** the runtime bundle, the activation credential and provider
|
||||
identity descriptors, the process write lease, the systemd credentials the
|
||||
binding names, and the child's health over its private listener.
|
||||
binding names, and the child's health over TCP at the member bind.
|
||||
- **Outputs:** signed `gamecult.runtime_presence_health.v2` (Warming, Active
|
||||
and failed to Odin; route observations on the candidate endpoint), one child
|
||||
process, and a rendered config in the unit's private tmp that is never read
|
||||
back.
|
||||
- **Route:** the launcher, not the child, binds the candidate endpoint. A first
|
||||
byte of 0x00 is an Idunn challenge frame and is answered by the launcher;
|
||||
every other byte is copied untouched to the child's private listener.
|
||||
- **Route:** Idunn owns both listeners of the route: the Expected route's
|
||||
stable endpoint (presence) and, when the binding has `[route.member]`, the
|
||||
member endpoint, rendered as a second server block in the one nginx fragment.
|
||||
The launcher binds the candidate endpoint and answers Idunn's challenge there,
|
||||
one connection at a time within a one-second deadline; any other first byte
|
||||
closes the connection. The child binds the member bind Idunn sets in
|
||||
`GAMECULT_IDUNN_MEMBER_BIND`, derived from the candidate port. Member bytes
|
||||
never touch the launcher.
|
||||
- **Lease:** with `write_lease_required` the launcher starts the child only
|
||||
after it holds the lease (bound to this Expected, this activation and one of
|
||||
its own Warming proofs) and keeps the lease store share-locked while the
|
||||
child lives. Losing the lease stops the child.
|
||||
- **Config:** one template from the release is rendered with the closed
|
||||
placeholder set `{{child.listen}}`, `{{credential:NAME}}`,
|
||||
placeholder set `{{child.listen}}`, `{{child.listen_host}}`,
|
||||
`{{child.listen_port}}` (all three the member bind), `{{credential:NAME}}`,
|
||||
`{{credential_path:NAME}}` and `{{state_root}}`. Any other placeholder
|
||||
refuses to start.
|
||||
- **Forbidden:** the child binding the candidate endpoint; a child of a
|
||||
lease-requiring target running without a held lease; any presence not signed
|
||||
- **Forbidden:** member traffic through the launcher; the child binding the
|
||||
candidate endpoint; a child of a lease-requiring target running without a held lease; any presence not signed
|
||||
by both keys.
|
||||
- **State kind `external-directory`:** a process-owned directory imposed by an
|
||||
external runtime contract. Same writer, recovery and startup rules as
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ fn main() -> std::process::ExitCode {
|
|||
use anyhow::Context;
|
||||
use idunn_daemon::deployment::{
|
||||
IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT, IDUNN_RUNTIME_BUNDLE_ENVIRONMENT,
|
||||
IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT,
|
||||
IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT, IDUNN_RUNTIME_MEMBER_BIND_ENVIRONMENT,
|
||||
};
|
||||
use idunn_daemon::launcher::{self, *};
|
||||
|
||||
|
|
@ -26,6 +26,11 @@ fn main() -> std::process::ExitCode {
|
|||
.context("the Idunn runtime bundle is not set")?;
|
||||
let bind = std::env::var(IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT)
|
||||
.context("the Idunn candidate bind is not set")?;
|
||||
let member = launcher::parse_member_bind(
|
||||
&std::env::var(IDUNN_RUNTIME_MEMBER_BIND_ENVIRONMENT)
|
||||
.context("the Idunn member bind is not set")?,
|
||||
&bind,
|
||||
)?;
|
||||
let (activation, provider) = launcher::inherited_authority_files()?;
|
||||
let authority = RuntimeAuthority::open(bundle.as_ref(), activation, provider)?;
|
||||
authority.require_candidate_bind(&bind)?;
|
||||
|
|
@ -47,10 +52,9 @@ fn main() -> std::process::ExitCode {
|
|||
sink: std::sync::Arc::new(sink),
|
||||
lease: std::sync::Arc::new(lease),
|
||||
spawner: Box::new(ProcessSpawner),
|
||||
route_bounds: RouteBounds::default(),
|
||||
health: Box::new(move |endpoint| {
|
||||
health: Box::new(move |member| {
|
||||
std::sync::Arc::new(HttpHealthProbe {
|
||||
endpoint: endpoint.clone(),
|
||||
member,
|
||||
path: health_path.clone(),
|
||||
})
|
||||
}),
|
||||
|
|
@ -64,6 +68,7 @@ fn main() -> std::process::ExitCode {
|
|||
&contract,
|
||||
authority,
|
||||
route,
|
||||
member,
|
||||
&std::env::temp_dir(),
|
||||
ports,
|
||||
&STOP,
|
||||
|
|
|
|||
|
|
@ -657,9 +657,7 @@ pub struct ProcessWriteLeaseBinding {
|
|||
|
||||
impl ProcessWriteLeaseBinding {
|
||||
pub fn lock_path(&self) -> PathBuf {
|
||||
let mut lock_path = self.record_path.as_os_str().to_os_string();
|
||||
lock_path.push(".lock");
|
||||
PathBuf::from(lock_path)
|
||||
crate::drivers::authority_lock_path(&self.record_path)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
865
src/launcher.rs
865
src/launcher.rs
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue