| 69 |
69 |
|
/// uploads from one host — so it runs a single `publish.rhai` rather than a
|
| 70 |
70 |
|
/// recipe per platform, and its `targets` name the host that does it.
|
| 71 |
71 |
|
Library,
|
| 72 |
|
- |
/// A long-running binary Bento ships onto the hosts that run it, rather than
|
| 73 |
|
- |
/// distributing to users. Built per target like an app, but it ends at
|
| 74 |
|
- |
/// `deploy` instead of `collect`: no bundle, no signature for a human to
|
| 75 |
|
- |
/// check, no registry. Where each target lands is `[[deploy]]` in the app's own manifest.
|
|
72 |
+ |
/// A long-running binary that is run rather than distributed to users: no
|
|
73 |
+ |
/// bundle, no signature for a human to check, no registry. Built per target
|
|
74 |
+ |
/// like an app.
|
|
75 |
+ |
///
|
|
76 |
+ |
/// The kind says what the thing IS, not who delivers it, and there are two
|
|
77 |
+ |
/// ways it reaches the host that runs it:
|
|
78 |
+ |
///
|
|
79 |
+ |
/// - `[[deploy]]` in the app's own manifest — Bento installs it and restarts
|
|
80 |
+ |
/// the unit, so the release ends at `deploy`.
|
|
81 |
+ |
/// - A `[handoff.<app>]` in the daemon config — Bento ends at `collect` and
|
|
82 |
+ |
/// hands the artifact to a Sando, which decides whether it advances a stage
|
|
83 |
+ |
/// (wiki `sando-bento-boundary`).
|
|
84 |
+ |
///
|
|
85 |
+ |
/// Exactly one, enforced by [`Topology::validate_delivery`]: neither is a
|
|
86 |
+ |
/// build that archives into nothing, both is two systems installing one
|
|
87 |
+ |
/// service with no rule for which wins.
|
| 76 |
88 |
|
///
|
| 77 |
89 |
|
/// A separate kind rather than an app with an extra step, because the two
|
| 78 |
90 |
|
/// differ in what `verify` means. An app's verify is Gatekeeper (is this
|
| 79 |
91 |
|
/// bundle notarized); a service's is running the toolchain against the
|
| 80 |
92 |
|
/// binary it just built — the same thing a library's crate preflight does.
|
|
93 |
+ |
/// That is also why a handed-off service is not simply `kind = "app"`: an
|
|
94 |
+ |
/// app's `verify` would demand a Gatekeeper grant from a Linux build host
|
|
95 |
+ |
/// that can never honestly hold one.
|
| 81 |
96 |
|
Service,
|
| 82 |
97 |
|
}
|
| 83 |
98 |
|
|
| 295 |
310 |
|
/// is the earlier, friendlier half of the same rule.
|
| 296 |
311 |
|
fn validate_deploy(name: &str, app: &AppConfig) -> Result<()> {
|
| 297 |
312 |
|
if app.deploy.is_empty() {
|
| 298 |
|
- |
anyhow::ensure!(
|
| 299 |
|
- |
app.kind != Kind::Service,
|
| 300 |
|
- |
"app `{name}` is a service but declares no [[deploy]] entries — \
|
| 301 |
|
- |
a service that lands nowhere has no release"
|
| 302 |
|
- |
);
|
|
313 |
+ |
// A service with no `[[deploy]]` used to be rejected outright. It is
|
|
314 |
+ |
// legal now, and means "somebody else installs this" — the Sando/Bento
|
|
315 |
+ |
// boundary, where Bento builds and packages and Sando decides whether a
|
|
316 |
+ |
// thing advances a stage. What is NOT legal is a service that neither
|
|
317 |
+ |
// deploys nor hands off, which is still "a service that lands nowhere".
|
|
318 |
+ |
//
|
|
319 |
+ |
// That second half cannot be checked here: it depends on the daemon's
|
|
320 |
+ |
// `[handoff]` tables, and this function only knows the app manifest. It
|
|
321 |
+ |
// is a cross-document invariant, so it lives where both documents are in
|
|
322 |
+ |
// hand — [`Topology::validate_delivery`], called at startup and by
|
|
323 |
+ |
// `--check-config`.
|
| 303 |
324 |
|
return Ok(());
|
| 304 |
325 |
|
}
|
| 305 |
326 |
|
anyhow::ensure!(
|
| 417 |
438 |
|
Self::resolve(toml::from_str(s)?)
|
| 418 |
439 |
|
}
|
| 419 |
440 |
|
|
|
441 |
+ |
/// Every service reaches the box that runs it exactly one way.
|
|
442 |
+ |
///
|
|
443 |
+ |
/// A service either installs itself (`[[deploy]]` in its own manifest) or is
|
|
444 |
+ |
/// handed to a Sando that does (`[handoff.<app>]` in the daemon config).
|
|
445 |
+ |
/// Neither is the original "a service that lands nowhere has no release":
|
|
446 |
+ |
/// it would build, archive, and stop, looking green while nothing shipped.
|
|
447 |
+ |
///
|
|
448 |
+ |
/// Both is refused too, and that is the more useful half. Under the boundary
|
|
449 |
+ |
/// (wiki `sando-bento-boundary`) deciding whether a thing advances a stage is
|
|
450 |
+ |
/// Sando's job, so a service that also installs itself has two systems with
|
|
451 |
+ |
/// an opinion about what is running and no rule for which wins. Better to
|
|
452 |
+ |
/// fail at startup than to discover it when a promote and a recipe disagree.
|
|
453 |
+ |
///
|
|
454 |
+ |
/// Cross-document, so it cannot live in `validate()`: the manifest is in the
|
|
455 |
+ |
/// app's repo and the handoff is in the daemon's config, and `Topology` only
|
|
456 |
+ |
/// parses the first. Called from `main` after both are loaded, which is also
|
|
457 |
+ |
/// what `--check-config` runs.
|
|
458 |
+ |
pub fn validate_delivery(&self, cfg: &crate::config::Config) -> Result<()> {
|
|
459 |
+ |
for (name, app) in &self.app {
|
|
460 |
+ |
if app.kind != Kind::Service {
|
|
461 |
+ |
continue;
|
|
462 |
+ |
}
|
|
463 |
+ |
let hands_off = cfg.handoff.contains_key(name.as_str());
|
|
464 |
+ |
match (app.deploy.is_empty(), hands_off) {
|
|
465 |
+ |
(true, false) => anyhow::bail!(
|
|
466 |
+ |
"app `{name}` is a service but neither declares [[deploy]] entries nor has \
|
|
467 |
+ |
a [handoff.{name}] table in the daemon config — it would build and archive \
|
|
468 |
+ |
and never reach the host that runs it"
|
|
469 |
+ |
),
|
|
470 |
+ |
(false, true) => anyhow::bail!(
|
|
471 |
+ |
"app `{name}` is a service that both declares [[deploy]] entries and has a \
|
|
472 |
+ |
[handoff.{name}] table — it would be installed by Bento AND handed to Sando \
|
|
473 |
+ |
to install. Pick one: Bento deploys it, or Sando does"
|
|
474 |
+ |
),
|
|
475 |
+ |
_ => {}
|
|
476 |
+ |
}
|
|
477 |
+ |
}
|
|
478 |
+ |
Ok(())
|
|
479 |
+ |
}
|
|
480 |
+ |
|
| 420 |
481 |
|
fn validate(&self) -> Result<()> {
|
| 421 |
482 |
|
anyhow::ensure!(
|
| 422 |
483 |
|
!self.hosts.is_empty(),
|
| 684 |
745 |
|
assert_eq!(mac.port, None);
|
| 685 |
746 |
|
}
|
| 686 |
747 |
|
|
| 687 |
|
- |
/// The kind and the `[[deploy]]` table have to agree in both directions. An
|
| 688 |
|
- |
/// app with deploy entries would build them and never install them; a
|
| 689 |
|
- |
/// service without any would have no terminal step at all.
|
|
748 |
+ |
/// `[[deploy]]` entries still mean "a service installs itself", so they stay
|
|
749 |
+ |
/// refused on anything that is not a service.
|
|
750 |
+ |
///
|
|
751 |
+ |
/// The other direction moved. A service with no `[[deploy]]` used to be
|
|
752 |
+ |
/// rejected here; it is now legal at the manifest level and means "somebody
|
|
753 |
+ |
/// else installs this", with `validate_delivery` deciding whether that
|
|
754 |
+ |
/// somebody exists. This function only sees the manifest, so it cannot know.
|
| 690 |
755 |
|
#[test]
|
| 691 |
|
- |
fn kind_and_deploy_entries_must_agree() {
|
|
756 |
+ |
fn deploy_entries_belong_only_to_a_service() {
|
| 692 |
757 |
|
let deploy = "\n[[deploy]]\ntarget = \"linux/x86_64\"\nhost = \"h\"\n\
|
| 693 |
758 |
|
install_path = \"/usr/local/bin/d\"\nservice = \"d.service\"\n";
|
| 694 |
|
- |
// Deploy entries on a plain app.
|
| 695 |
759 |
|
assert!(
|
| 696 |
760 |
|
load_with(HOSTS, &format!("targets = [\"linux/x86_64\"]\n{deploy}")).is_err(),
|
| 697 |
761 |
|
"only a service installs onto a host"
|
| 698 |
762 |
|
);
|
| 699 |
|
- |
// A service with none.
|
| 700 |
763 |
|
assert!(
|
| 701 |
|
- |
load_with(HOSTS, "kind = \"service\"\ntargets = [\"linux/x86_64\"]\n").is_err(),
|
| 702 |
|
- |
"a service that lands nowhere has no release"
|
|
764 |
+ |
load_with(HOSTS, "kind = \"service\"\ntargets = [\"linux/x86_64\"]\n").is_ok(),
|
|
765 |
+ |
"a service with no [[deploy]] parses; whether it hands off is validate_delivery's call"
|
| 703 |
766 |
|
);
|
| 704 |
767 |
|
}
|
| 705 |
768 |
|
|
|
769 |
+ |
/// A service reaches its host exactly one way. Neither route is a build that
|
|
770 |
+ |
/// archives into nothing; both routes is two systems installing one service
|
|
771 |
+ |
/// with no rule for which wins.
|
|
772 |
+ |
#[test]
|
|
773 |
+ |
fn a_service_must_deploy_itself_or_hand_off_but_not_both() {
|
|
774 |
+ |
let tmp = tempfile::tempdir().unwrap();
|
|
775 |
+ |
let mut cfg = crate::config::Config::for_tests(tmp.path());
|
|
776 |
+ |
|
|
777 |
+ |
let (handing_off, _keep) =
|
|
778 |
+ |
load_with(HOSTS, "kind = \"service\"\ntargets = [\"linux/x86_64\"]\n").unwrap();
|
|
779 |
+ |
let name = handing_off.app.keys().next().unwrap().clone();
|
|
780 |
+ |
|
|
781 |
+ |
// Neither: refused, and the message says what would have happened.
|
|
782 |
+ |
let err = handing_off.validate_delivery(&cfg).unwrap_err();
|
|
783 |
+ |
assert!(
|
|
784 |
+ |
format!("{err:#}").contains("never reach the host"),
|
|
785 |
+ |
"{err:#}"
|
|
786 |
+ |
);
|
|
787 |
+ |
|
|
788 |
+ |
// Handed off: fine. This is pom under the boundary.
|
|
789 |
+ |
cfg.handoff.insert(
|
|
790 |
+ |
name.clone(),
|
|
791 |
+ |
crate::config::Handoff {
|
|
792 |
+ |
host: "local".into(),
|
|
793 |
+ |
staging_root: "/srv/sando/staging".into(),
|
|
794 |
+ |
url: "http://127.0.0.1:7766".into(),
|
|
795 |
+ |
sando_app: None,
|
|
796 |
+ |
token_file: None,
|
|
797 |
+ |
},
|
|
798 |
+ |
);
|
|
799 |
+ |
handing_off.validate_delivery(&cfg).unwrap();
|
|
800 |
+ |
|
|
801 |
+ |
// Both: refused. Bento would install it and Sando would too.
|
|
802 |
+ |
let (self_deploying, _keep2) = load_with(
|
|
803 |
+ |
HOSTS,
|
|
804 |
+ |
"kind = \"service\"\ntargets = [\"linux/x86_64\"]\n\
|
|
805 |
+ |
[[deploy]]\ntarget = \"linux/x86_64\"\nhost = \"h\"\n\
|
|
806 |
+ |
install_path = \"/usr/local/bin/d\"\nservice = \"d.service\"\n",
|
|
807 |
+ |
)
|
|
808 |
+ |
.unwrap();
|
|
809 |
+ |
let err = self_deploying.validate_delivery(&cfg).unwrap_err();
|
|
810 |
+ |
assert!(format!("{err:#}").contains("Pick one"), "{err:#}");
|
|
811 |
+ |
|
|
812 |
+ |
// And a non-service is never subject to any of it.
|
|
813 |
+ |
let (plain, _keep3) = load_with(HOSTS, "targets = [\"linux/x86_64\"]\n").unwrap();
|
|
814 |
+ |
plain.validate_delivery(&cfg).unwrap();
|
|
815 |
+ |
}
|
|
816 |
+ |
|
| 706 |
817 |
|
/// Every target a service ships must say where it lands. Without this a
|
| 707 |
818 |
|
/// half-configured service builds both arches and silently installs one.
|
| 708 |
819 |
|
#[test]
|