remove the legacy meta repo's .git and the meta remotes at startup
This commit is contained in:
parent
ca6fc87566
commit
0f4566c8d3
4 changed files with 110 additions and 5 deletions
|
|
@ -1,5 +1,6 @@
|
|||
//! Startup convergence. Three phases, all idempotent and unguarded:
|
||||
//! harness files, applied + proposed repos, the agent-set files. They re-run every
|
||||
//! harness files, applied + proposed repos, the agent-set files, plus the
|
||||
//! removal of the legacy meta repo's leftovers. They re-run every
|
||||
//! boot on purpose — each one is a no-op once its state is already
|
||||
//! correct.
|
||||
//!
|
||||
|
|
@ -68,6 +69,12 @@ pub async fn run() -> Result<()> {
|
|||
migrate_harness_files(name);
|
||||
}
|
||||
|
||||
match remove_legacy_meta_git(&crate::paths::meta_root()) {
|
||||
Ok(true) => tracing::info!("migration: removed the legacy meta repo's .git"),
|
||||
Ok(false) => {}
|
||||
Err(e) => tracing::warn!(error = ?e, "migration: removing the legacy meta .git failed"),
|
||||
}
|
||||
|
||||
// Phase 1 + 2: per-agent applied + proposed.
|
||||
tracing::debug!("migration: phase 1+2 (applied + proposed repos)");
|
||||
// An archetype agent has neither repo, and seeding one here would hand it
|
||||
|
|
@ -85,6 +92,13 @@ pub async fn run() -> Result<()> {
|
|||
tracing::warn!(%name, error = ?e, "migration: applied repo rewrite failed");
|
||||
}
|
||||
let proposed_dir = Coordinator::agent_proposed_dir(name);
|
||||
match remove_meta_remote(&proposed_dir).await {
|
||||
Ok(true) => tracing::info!(%name, "migration: removed the legacy meta remote"),
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
tracing::warn!(%name, error = ?e, "migration: removing the meta remote failed");
|
||||
}
|
||||
}
|
||||
let proposed = lifecycle::setup_proposed(&proposed_dir, name.as_str());
|
||||
match tokio::time::timeout(GIT_TIMEOUT, proposed).await {
|
||||
Ok(Err(e)) => tracing::warn!(%name, error = ?e, "migration: setup_proposed failed"),
|
||||
|
|
@ -148,6 +162,31 @@ fn migrate_harness_files(name: &hive_types::Ident) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Remove the `.git` of the legacy per-hive meta repo from `meta`, leaving
|
||||
/// the plain state beside it (`topology.json`, the swarm limits, any
|
||||
/// permission seed file not yet imported). `true` when it was there.
|
||||
fn remove_legacy_meta_git(meta: &Path) -> Result<bool> {
|
||||
let git_dir = meta.join(".git");
|
||||
match std::fs::remove_dir_all(&git_dir) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
||||
Err(e) => Err(e).with_context(|| format!("removing {}", git_dir.display())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the `meta` remote from the proposed repo at `dir`. `true` when it
|
||||
/// was there; a missing remote, or a `dir` that is no repo, is `false`.
|
||||
async fn remove_meta_remote(dir: &Path) -> Result<bool> {
|
||||
let mut cmd = lifecycle::git_command();
|
||||
cmd.current_dir(dir).args(["remote", "get-url", "meta"]);
|
||||
let probe = output_with_timeout(cmd, GIT_TIMEOUT, "git remote get-url meta").await?;
|
||||
if !probe.status.success() {
|
||||
return Ok(false);
|
||||
}
|
||||
raw_git(dir, &["remote", "remove", "meta"]).await?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn enumerate_agents() -> Result<Vec<hive_types::Ident>> {
|
||||
let containers = lifecycle::list().await?;
|
||||
Ok(containers
|
||||
|
|
@ -248,6 +287,67 @@ async fn raw_git(dir: &Path, args: &[&str]) -> Result<()> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const STATE_FILES: [&str; 3] = [
|
||||
"topology.json",
|
||||
"swarm-resource-limits.json",
|
||||
"capabilities.json",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn legacy_meta_git_goes_and_the_state_beside_it_stays() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
std::fs::create_dir_all(dir.path().join(".git/objects")).expect("mkdir .git");
|
||||
for name in STATE_FILES {
|
||||
std::fs::write(dir.path().join(name), "{}").expect("write state");
|
||||
}
|
||||
|
||||
assert!(remove_legacy_meta_git(dir.path()).expect("first"));
|
||||
assert!(!dir.path().join(".git").exists());
|
||||
for name in STATE_FILES {
|
||||
assert!(dir.path().join(name).exists(), "{name} stays");
|
||||
}
|
||||
assert!(
|
||||
!remove_legacy_meta_git(dir.path()).expect("second"),
|
||||
"no-op"
|
||||
);
|
||||
assert!(dir.path().join("topology.json").exists());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn meta_remote_goes_and_the_others_stay() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
raw_git(dir.path(), &["init", "-q", "--initial-branch=main"])
|
||||
.await
|
||||
.expect("init");
|
||||
for (name, url) in [
|
||||
("meta", "http://forge/core/meta.git"),
|
||||
("applied", "/applied/x/.git"),
|
||||
] {
|
||||
raw_git(dir.path(), &["remote", "add", name, url])
|
||||
.await
|
||||
.expect("add remote");
|
||||
}
|
||||
|
||||
assert!(remove_meta_remote(dir.path()).await.expect("first"));
|
||||
let remotes = lifecycle::git_command()
|
||||
.current_dir(dir.path())
|
||||
.arg("remote")
|
||||
.output()
|
||||
.await
|
||||
.expect("list");
|
||||
assert_eq!(String::from_utf8_lossy(&remotes.stdout).trim(), "applied");
|
||||
assert!(
|
||||
!remove_meta_remote(dir.path()).await.expect("second"),
|
||||
"no-op"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_dir_that_is_no_repo_has_no_meta_remote() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
assert!(!remove_meta_remote(dir.path()).await.expect("probe"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unreadable_flake_is_left_untouched() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
|
|
|
|||
Loading…
Reference in a new issue