| 3951 |
3951 |
|
sqlx::query("INSERT INTO versions (version, git_sha, built_at, artifact_path) VALUES (?, 'sha', datetime('now'), '/tmp/x') ON CONFLICT DO NOTHING")
|
| 3952 |
3952 |
|
.bind(version).execute(pool).await.unwrap();
|
| 3953 |
3953 |
|
}
|
|
3954 |
+ |
|
|
3955 |
+ |
// ---- per-node bundle resolution (wiki release-artifact-identity) ----
|
|
3956 |
+ |
|
|
3957 |
+ |
/// A settled build of `version` recorded as `platform`'s half of it.
|
|
3958 |
+ |
async fn seed_platform_build(
|
|
3959 |
+ |
pool: &SqlitePool,
|
|
3960 |
+ |
sha: &str,
|
|
3961 |
+ |
version: &str,
|
|
3962 |
+ |
platform: &str,
|
|
3963 |
+ |
staged_path: &str,
|
|
3964 |
+ |
) -> i64 {
|
|
3965 |
+ |
sqlx::query_scalar(
|
|
3966 |
+ |
"INSERT INTO build_runs (sha, version, phase, result, started_at, bundle_digest, staged_path, platform)
|
|
3967 |
+ |
VALUES (?, ?, 'done', 'passed', datetime('now'), ?, ?, ?) RETURNING id",
|
|
3968 |
+ |
)
|
|
3969 |
+ |
.bind(sha)
|
|
3970 |
+ |
.bind(version)
|
|
3971 |
+ |
.bind(format!("{sha}-digest"))
|
|
3972 |
+ |
.bind(staged_path)
|
|
3973 |
+ |
.bind(platform)
|
|
3974 |
+ |
.fetch_one(pool)
|
|
3975 |
+ |
.await
|
|
3976 |
+ |
.unwrap()
|
|
3977 |
+ |
}
|
|
3978 |
+ |
|
|
3979 |
+ |
fn node_on(name: &str, platform: Option<&str>) -> Node {
|
|
3980 |
+ |
Node {
|
|
3981 |
+ |
platform: platform.map(plat),
|
|
3982 |
+ |
name: name.into(),
|
|
3983 |
+ |
ssh_target: "local".into(),
|
|
3984 |
+ |
release_root: format!("/tmp/{name}"),
|
|
3985 |
+ |
service_name: "makenotwork.service".into(),
|
|
3986 |
+ |
health_url: None,
|
|
3987 |
+ |
config_check_env_file: None,
|
|
3988 |
+ |
actuate: crate::topology::default_actuate(),
|
|
3989 |
+ |
observe: crate::topology::default_observe(),
|
|
3990 |
+ |
companions: Vec::new(),
|
|
3991 |
+ |
}
|
|
3992 |
+ |
}
|
|
3993 |
+ |
|
|
3994 |
+ |
/// One pom version is two bundles with two digests. The node states which
|
|
3995 |
+ |
/// architecture it can run, and the sibling bundle is resolved out of
|
|
3996 |
+ |
/// `build_runs` rather than the caller's own half being shipped everywhere.
|
|
3997 |
+ |
#[tokio::test]
|
|
3998 |
+ |
async fn a_node_on_the_other_architecture_gets_its_own_bundle() {
|
|
3999 |
+ |
let state = test_state().await;
|
|
4000 |
+ |
seed_version(&state.pool, "5.0.0").await;
|
|
4001 |
+ |
let arm = seed_platform_build(
|
|
4002 |
+ |
&state.pool,
|
|
4003 |
+ |
"sha-arm",
|
|
4004 |
+ |
"5.0.0",
|
|
4005 |
+ |
"linux/aarch64",
|
|
4006 |
+ |
"/rel/aaaaaaaaaaaaaaaa",
|
|
4007 |
+ |
)
|
|
4008 |
+ |
.await;
|
|
4009 |
+ |
let x86 = seed_platform_build(
|
|
4010 |
+ |
&state.pool,
|
|
4011 |
+ |
"sha-x86",
|
|
4012 |
+ |
"5.0.0",
|
|
4013 |
+ |
"linux/x86_64",
|
|
4014 |
+ |
"/rel/bbbbbbbbbbbbbbbb",
|
|
4015 |
+ |
)
|
|
4016 |
+ |
.await;
|
|
4017 |
+ |
|
|
4018 |
+ |
let n_arm = node_on("astra", Some("linux/aarch64"));
|
|
4019 |
+ |
let n_x86 = node_on("hetzner", Some("linux/x86_64"));
|
|
4020 |
+ |
let bundles = super::promotion::bundles_for_nodes(
|
|
4021 |
+ |
&state,
|
|
4022 |
+ |
"5.0.0",
|
|
4023 |
+ |
&[&n_arm, &n_x86],
|
|
4024 |
+ |
std::path::Path::new("/rel/aaaaaaaaaaaaaaaa"),
|
|
4025 |
+ |
Some(&plat("linux/aarch64")),
|
|
4026 |
+ |
Some(arm),
|
|
4027 |
+ |
)
|
|
4028 |
+ |
.await
|
|
4029 |
+ |
.expect("both architectures have a green bundle at this version");
|
|
4030 |
+ |
|
|
4031 |
+ |
assert_eq!(bundles.len(), 2);
|
|
4032 |
+ |
assert_eq!(
|
|
4033 |
+ |
bundles[0].1,
|
|
4034 |
+ |
std::path::PathBuf::from("/rel/aaaaaaaaaaaaaaaa")
|
|
4035 |
+ |
);
|
|
4036 |
+ |
assert_eq!(bundles[0].2, Some(plat("linux/aarch64")));
|
|
4037 |
+ |
assert_eq!(bundles[0].3, Some(arm));
|
|
4038 |
+ |
// The one the caller never held: resolved by platform, and it carries
|
|
4039 |
+ |
// the sibling's own build id so its own gate evidence is what gets
|
|
4040 |
+ |
// checked.
|
|
4041 |
+ |
assert_eq!(
|
|
4042 |
+ |
bundles[1].1,
|
|
4043 |
+ |
std::path::PathBuf::from("/rel/bbbbbbbbbbbbbbbb")
|
|
4044 |
+ |
);
|
|
4045 |
+ |
assert_eq!(bundles[1].2, Some(plat("linux/x86_64")));
|
|
4046 |
+ |
assert_eq!(bundles[1].3, Some(x86));
|
|
4047 |
+ |
}
|
|
4048 |
+ |
|
|
4049 |
+ |
/// A version with no green bundle for the node's architecture fails the
|
|
4050 |
+ |
/// whole resolution, before any node is touched.
|
|
4051 |
+ |
#[tokio::test]
|
|
4052 |
+ |
async fn a_missing_architecture_half_refuses_the_promote_rather_than_defaulting() {
|
|
4053 |
+ |
let state = test_state().await;
|
|
4054 |
+ |
seed_version(&state.pool, "5.0.0").await;
|
|
4055 |
+ |
seed_platform_build(
|
|
4056 |
+ |
&state.pool,
|
|
4057 |
+ |
"sha-arm",
|
|
4058 |
+ |
"5.0.0",
|
|
4059 |
+ |
"linux/aarch64",
|
|
4060 |
+ |
"/rel/aaaaaaaaaaaaaaaa",
|
|
4061 |
+ |
)
|
|
4062 |
+ |
.await;
|
|
4063 |
+ |
let n_x86 = node_on("hetzner", Some("linux/x86_64"));
|
|
4064 |
+ |
let err = super::promotion::bundles_for_nodes(
|
|
4065 |
+ |
&state,
|
|
4066 |
+ |
"5.0.0",
|
|
4067 |
+ |
&[&n_x86],
|
|
4068 |
+ |
std::path::Path::new("/rel/aaaaaaaaaaaaaaaa"),
|
|
4069 |
+ |
Some(&plat("linux/aarch64")),
|
|
4070 |
+ |
None,
|
|
4071 |
+ |
)
|
|
4072 |
+ |
.await
|
|
4073 |
+ |
.expect_err("the x86_64 half was never built, so there is nothing to ship");
|
|
4074 |
+ |
assert!(
|
|
4075 |
+ |
format!("{err:?}").contains("no green linux/x86_64 bundle"),
|
|
4076 |
+ |
"{err:?}"
|
|
4077 |
+ |
);
|
|
4078 |
+ |
}
|
|
4079 |
+ |
|
|
4080 |
+ |
/// When the node and the caller name the same platform, the caller's bundle
|
|
4081 |
+ |
/// is the answer and no lookup happens. A single-platform product whose
|
|
4082 |
+ |
/// nodes have started stating a platform still has no `build_runs.platform`
|
|
4083 |
+ |
/// row to find, so a lookup here would refuse a promote that is fine.
|
|
4084 |
+ |
#[tokio::test]
|
|
4085 |
+ |
async fn a_node_that_agrees_with_the_caller_takes_the_callers_bundle() {
|
|
4086 |
+ |
let state = test_state().await;
|
|
4087 |
+ |
seed_version(&state.pool, "5.0.0").await;
|
|
4088 |
+ |
let node = node_on("hetzner", Some("linux/x86_64"));
|
|
4089 |
+ |
let bundles = super::promotion::bundles_for_nodes(
|
|
4090 |
+ |
&state,
|
|
4091 |
+ |
"5.0.0",
|
|
4092 |
+ |
&[&node],
|
|
4093 |
+ |
std::path::Path::new("/rel/legacy"),
|
|
4094 |
+ |
Some(&plat("linux/x86_64")),
|
|
4095 |
+ |
None,
|
|
4096 |
+ |
)
|
|
4097 |
+ |
.await
|
|
4098 |
+ |
.expect("the caller already holds the bundle this node wants");
|
|
4099 |
+ |
assert_eq!(bundles[0].1, std::path::PathBuf::from("/rel/legacy"));
|
|
4100 |
+ |
assert_eq!(bundles[0].2, Some(plat("linux/x86_64")));
|
|
4101 |
+ |
assert_eq!(bundles[0].3, None);
|
|
4102 |
+ |
}
|
|
4103 |
+ |
|
|
4104 |
+ |
/// The previous version is two bundles too. If the one this node runs
|
|
4105 |
+ |
/// cannot be resolved, nothing is attempted anywhere and every touched node
|
|
4106 |
+ |
/// is indeterminate — which is the truth, not a default.
|
|
4107 |
+ |
#[tokio::test]
|
|
4108 |
+ |
async fn a_rollback_that_cannot_resolve_a_bundle_reports_every_node_indeterminate() {
|
|
4109 |
+ |
let state = test_state().await;
|
|
4110 |
+ |
sqlx::query("INSERT INTO versions (version, git_sha, built_at, artifact_path) VALUES ('1.0.0','sha',datetime('now'),'/tmp/staged/releases/1.0.0/makenotwork')")
|
|
4111 |
+ |
.execute(&state.pool).await.unwrap();
|
|
4112 |
+ |
// No build_runs row for either node's architecture at 1.0.0.
|
|
4113 |
+ |
let n1 = node_on("n1", Some("linux/aarch64"));
|
|
4114 |
+ |
let n2 = node_on("n2", Some("linux/aarch64"));
|
|
4115 |
+ |
let report = rollback_deployed_nodes(&state, &tid("a"), &[&n1, &n2], "1.0.0").await;
|
|
4116 |
+ |
assert_eq!(report.restored, 0);
|
|
4117 |
+ |
assert_eq!(
|
|
4118 |
+ |
report.indeterminate, 2,
|
|
4119 |
+ |
"one per node the rollback never reached: {report:?}"
|
|
4120 |
+ |
);
|
|
4121 |
+ |
assert!(!report.is_consistent());
|
|
4122 |
+ |
}
|
|
4123 |
+ |
|
|
4124 |
+ |
/// A rollback that fails at the symlink swap on one node of several leaves
|
|
4125 |
+ |
/// that node unknown and the rest restored. The count has to be per node:
|
|
4126 |
+ |
/// a tier reported wholesale indeterminate sends an operator to inspect
|
|
4127 |
+ |
/// boxes that are fine, and a tier reported wholesale restored hides the
|
|
4128 |
+ |
/// one that is not.
|
|
4129 |
+ |
#[tokio::test]
|
|
4130 |
+ |
async fn a_rollback_failing_on_one_node_counts_only_that_node_indeterminate() {
|
|
4131 |
+ |
// The marker matches the swap-and-restart script, which is the only op
|
|
4132 |
+ |
// annotated AtOrAfterSwap; a1's rollback therefore lands in the
|
|
4133 |
+ |
// indeterminate arm rather than the already-on-previous one.
|
|
4134 |
+ |
let (state, _log) = fleet_fixture(&["a1", "a2"], Some("a1"), "reload-or-restart").await;
|
|
4135 |
+ |
let nodes: Vec<Node> = state.topo.tiers[1].nodes.clone();
|
|
4136 |
+ |
let refs: Vec<&Node> = nodes.iter().collect();
|
|
4137 |
+ |
|
|
4138 |
+ |
let report = rollback_deployed_nodes(&state, &tid("a"), &refs, "1.0.0").await;
|
|
4139 |
+ |
assert_eq!(report.restored, 1, "a2 came back: {report:?}");
|
|
4140 |
+ |
assert_eq!(
|
|
4141 |
+ |
report.indeterminate, 1,
|
|
4142 |
+ |
"only the node whose swap failed is unknown: {report:?}"
|
|
4143 |
+ |
);
|
|
4144 |
+ |
assert_eq!(report.already_on_previous, 0);
|
|
4145 |
+ |
assert_eq!(report.touched(), 2);
|
|
4146 |
+ |
assert!(!report.is_consistent());
|
|
4147 |
+ |
}
|
| 3954 |
4148 |
|
}
|