crates/kernel/ents-effect/src/lib.rs
| 1 | //! Effect execution, results, and toolchains at run time (`docs/spec/effect.adoc`): |
| 2 | //! the `Executor` trait, its Docker and Sprite backends, toolchain |
| 3 | //! materialization, and the run loop that ties them to |
| 4 | //! [`ents_receive::receive`] as the sole path a result re-enters the |
| 5 | //! repository. |
| 6 | //! |
| 7 | //! This crate closes the loop `ents-gate` and `ents-receive` open |
| 8 | //! (`docs/abstractions.adoc`, "The loop"): an effect's trigger is |
| 9 | //! evaluated by `ents-query` (already linked by `ents-receive` for |
| 10 | //! footprint matching, never by this crate's own dependents in the other |
| 11 | //! direction — `arch.query-effect-split`), its run happens behind one |
| 12 | //! [`Executor`] seam with multiple backends, and its outcome returns as an |
| 13 | //! ordinary signed commit through [`write_result`], a `receive` client |
| 14 | //! exactly like the CLI or a web edit. |
| 15 | //! |
| 16 | //! # Spec coverage |
| 17 | //! |
| 18 | //! From `docs/spec/effect.adoc`: |
| 19 | //! |
| 20 | //! - `effect.definition`, `effect.admin-only` — already carried by |
| 21 | //! `ents-model`'s [`ents_model::Effect`] and `ents-gate`'s default |
| 22 | //! authorization arm; nothing new here. |
| 23 | //! - `effect.validation` — [`definition::validate`]. `ents-receive` cannot |
| 24 | //! call this itself (`arch.query-effect-split`); a future frontend that |
| 25 | //! builds an effect-definition commit does, before ever proposing the |
| 26 | //! write. |
| 27 | //! - `effect.execution`, `effect.deployment-property` — [`Executor`], |
| 28 | //! [`SandboxInputs`], [`RunOutput`]; [`docker::DockerExecutor`] (feature |
| 29 | //! `docker`), [`sprite::SpriteExecutor`] (feature `sprite`), |
| 30 | //! [`UnsandboxedExecutor`]. No executor, sandbox, or retry choice is |
| 31 | //! readable from an [`ents_model::Effect`] — every backend is |
| 32 | //! constructed and selected only by a composition root. |
| 33 | //! - `effect.local-run` — [`run::run_one`] is the single code path |
| 34 | //! [`run::run_effect`] (the boot-time/on-demand form, no queue) and a |
| 35 | //! future hosted worker (a queue drain feeding the same [`run::run_one`] |
| 36 | //! calls) both use. |
| 37 | //! - `effect.results-writeback`, `effect.result-taxonomy` — |
| 38 | //! [`write_result`]: an ordinary [`ents_receive::receive`] client, |
| 39 | //! landing exactly `pass`/`fail`/`error` on |
| 40 | //! `refs/meta/results/<effect>/<short-oid>` |
| 41 | //! ([`run::short_oid`]). This crate never writes `Status::Error` itself |
| 42 | //! — see [`Error`]'s own doc for why an infrastructure failure is always |
| 43 | //! an `Err`, never a taxonomy value this crate chooses on a caller's |
| 44 | //! behalf. |
| 45 | //! - `effect.identity` — [`write_result`] takes a `sign` closure the |
| 46 | //! composition root injects (mirrors `ents_sync::resolve::merge_heads`); |
| 47 | //! this crate never holds key material. |
| 48 | //! - `effect.official` — a refname-authorization rule on canonical |
| 49 | //! `refs/meta/results/<effect>/*`, owned by `ents-gate`'s future |
| 50 | //! Config-driven worker-key narrowing (see that crate's own doc); this |
| 51 | //! crate only chooses *which* refname to target |
| 52 | //! ([`run::run_one`]'s `results_ref`), never judges authorization. |
| 53 | //! - `effect.self-run` — [`write_result`] and [`run::run_effect`] accept |
| 54 | //! any results refname, canonical or |
| 55 | //! [`ents_model::namespace::self_result_ref`]; adopting a self-run |
| 56 | //! result onto the canonical ref is `ents-sync`'s adoption merge |
| 57 | //! (`gate.adoption-merge`, `sync.adoption-machinery`), unchanged by this |
| 58 | //! crate. |
| 59 | //! - `effect.toolchains`, `model.toolchain` — moved to `ents-kiln` |
| 60 | //! (`Toolchain`, `Recipe`, `Component`, `toolchain::resolve`, |
| 61 | //! `toolchain::materialize`): this crate's own contract is now just that |
| 62 | //! [`SandboxInputs::toolchains`] is a pre-materialized slice; resolving |
| 63 | //! declared names to that slice is the composition root's job, done via |
| 64 | //! `ents-kiln`. Only [`Executor::run`]'s sandbox ever touches the |
| 65 | //! materialized bytes this crate hands it. |
| 66 | //! - `effect.fanout-index` — structurally satisfied, no dedicated code: a |
| 67 | //! fanout-index rebuild is an ordinary effect (`run`ning `git index |
| 68 | //! rebuild` or similar, a later, unbuilt command), so it uses exactly |
| 69 | //! the same [`Executor`] and [`write_result`] path as any other effect. |
| 70 | //! |
| 71 | //! # Examples |
| 72 | //! |
| 73 | //! An end-to-end local run: enroll a worker, define an effect and a |
| 74 | //! (trivial, embedded-empty) toolchain, advance a code ref, and run the |
| 75 | //! effect with a stub executor — the shape `effect.local-run` names, minus |
| 76 | //! only a real sandbox. |
| 77 | //! |
| 78 | //! ``` |
| 79 | //! use ents_effect::run::{run_effect, short_oid}; |
| 80 | //! use ents_effect::{Executor, RunOutput, RunStatus, SandboxInputs}; |
| 81 | //! use ents_model::{Effect, Provenance, namespace}; |
| 82 | //! use ents_receive::{Mode, NullEventSink}; |
| 83 | //! use ents_testutil::{Keypair, MemRefStore, ObjectStore, advance_ref, enroll_member}; |
| 84 | //! use gix_ref_store::RefStoreRead as _; |
| 85 | //! |
| 86 | //! struct AlwaysPass; |
| 87 | //! impl Executor for AlwaysPass { |
| 88 | //! fn run(&self, _inputs: &SandboxInputs<'_>) -> ents_effect::Result<RunOutput> { |
| 89 | //! Ok(RunOutput { status: RunStatus::Pass, log: "ok".into() }) |
| 90 | //! } |
| 91 | //! } |
| 92 | //! |
| 93 | //! let refs = MemRefStore::default(); |
| 94 | //! let objects = ObjectStore::default(); |
| 95 | //! let worker = Keypair::from_seed(1); |
| 96 | //! enroll_member(&refs, &objects, "worker", &worker, Provenance::AdminRegistered, 100); |
| 97 | //! |
| 98 | //! // No declared toolchains: resolving names to materialized directories |
| 99 | //! // is the composition root's job (via `ents-kiln`), not this crate's — |
| 100 | //! // `run_effect` only ever receives an already-materialized slice. |
| 101 | //! let effect = Effect { |
| 102 | //! name: "unit".into(), |
| 103 | //! trigger: "rev(refs/heads/main)".into(), |
| 104 | //! toolchains: vec![], |
| 105 | //! run: "true".into(), |
| 106 | //! }; |
| 107 | //! assert!(ents_effect::definition::validate(&effect).is_ok()); |
| 108 | //! |
| 109 | //! let commits = advance_ref(&refs, &objects, "refs/heads/main", 1, 200); |
| 110 | //! |
| 111 | //! let author = gix::actor::Signature { |
| 112 | //! name: "worker".into(), email: "worker@ents.test".into(), |
| 113 | //! time: gix::date::Time { seconds: 300, offset: 0 }, |
| 114 | //! }; |
| 115 | //! let scratch = tempfile::tempdir().expect("tempdir"); |
| 116 | //! |
| 117 | //! let outcomes = run_effect( |
| 118 | //! &refs, &objects, &NullEventSink, &AlwaysPass, scratch.path(), &[], |
| 119 | //! "unit", &effect, None, |
| 120 | //! |short| Ok(namespace::result_ref("unit", short).expect("valid")), |
| 121 | //! &author, &|payload| worker.sign(payload), Mode::Advisory, |
| 122 | //! ).expect("runs"); |
| 123 | //! |
| 124 | //! assert_eq!(outcomes.len(), 1); |
| 125 | //! assert_eq!(outcomes[0].0, commits[0]); |
| 126 | //! assert_eq!(outcomes[0].1.result, ents_receive::TxResult::Applied); |
| 127 | //! |
| 128 | //! // The canonical results ref now carries a pass. |
| 129 | //! let name = namespace::result_ref("unit", &short_oid(commits[0])).expect("valid"); |
| 130 | //! assert!(refs.get(name.as_ref()).expect("readable").is_some()); |
| 131 | //! ``` |
| 132 | |
| 133 | pub mod definition; |
| 134 | mod error; |
| 135 | pub mod executor; |
| 136 | pub mod materialize; |
| 137 | mod results; |
| 138 | pub mod run; |
| 139 | mod unsandboxed; |
| 140 | |
| 141 | #[cfg(feature = "docker")] |
| 142 | pub mod docker; |
| 143 | #[cfg(feature = "sprite")] |
| 144 | pub mod sprite; |
| 145 | |
| 146 | pub use error::{Error, Result}; |
| 147 | pub use executor::{Executor, RunOutput, RunStatus, SandboxInputs}; |
| 148 | pub use results::write_result; |
| 149 | pub use run::{run_effect, run_one}; |
| 150 | pub use unsandboxed::UnsandboxedExecutor; |
| 151 | |
| 152 | #[cfg(feature = "docker")] |
| 153 | pub use docker::DockerExecutor; |
| 154 | #[cfg(feature = "sprite")] |
| 155 | pub use sprite::SpriteExecutor; |