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
|
|
@ -338,6 +338,10 @@ once already applied:
|
||||||
repo, and refreshes the files that follow the agent set
|
repo, and refreshes the files that follow the agent set
|
||||||
(`agent_set::refresh`) from the current agent list. Set
|
(`agent_set::refresh`) from the current agent list. Set
|
||||||
`HIVE_SKIP_META_MIGRATION=1` on the service to defer this phase.
|
`HIVE_SKIP_META_MIGRATION=1` on the service to defer this phase.
|
||||||
|
- **Legacy meta repo**: deletes the leftover `.git` in
|
||||||
|
`/var/lib/hyperhive/meta/` and the `meta` remote in each proposed repo.
|
||||||
|
The files beside the `.git` stay. The `core/meta` repo on the forge is
|
||||||
|
not touched.
|
||||||
|
|
||||||
No state loss in either migration: claude creds, `/state/` notes, the
|
No state loss in either migration: claude creds, `/state/` notes, the
|
||||||
events DB, and both proposed + applied history all survive. The root
|
events DB, and both proposed + applied history all survive. The root
|
||||||
|
|
|
||||||
|
|
@ -388,8 +388,9 @@ here from merged config PRs) + any other files those PRs commit.
|
||||||
denied tag history.
|
denied tag history.
|
||||||
|
|
||||||
Under `/var/lib/hyperhive/meta/` — system-level config files, plain state
|
Under `/var/lib/hyperhive/meta/` — system-level config files, plain state
|
||||||
with no git history that anything reads. The flake agents build from is the
|
with no git history. The flake agents build from is the
|
||||||
swarm's `internal/meta`, not here. Contents:
|
swarm's `internal/meta`, not here. A hive that predates this finds a
|
||||||
|
leftover `.git` here; `migrate::run` deletes it at startup. Contents:
|
||||||
|
|
||||||
- `topology.json` — the agent roster (`["alice", "bob", "ruth"]`).
|
- `topology.json` — the agent roster (`["alice", "bob", "ruth"]`).
|
||||||
Written by `topology::reconcile` from `agent_set::refresh`; read by
|
Written by `topology::reconcile` from `agent_set::refresh`; read by
|
||||||
|
|
|
||||||
|
|
@ -320,8 +320,8 @@ The hive keeps no flake of its own. `meta_rev.rs` picks the `internal/meta`
|
||||||
commit an agent builds from and records it; `agent_set.rs` keeps the
|
commit an agent builds from and records it; `agent_set.rs` keeps the
|
||||||
host-side files that follow the agent set (`topology.json`, the gateway's
|
host-side files that follow the agent set (`topology.json`, the gateway's
|
||||||
agent-socket map and nginx include) in line with the live agents, and
|
agent-socket map and nginx include) in line with the live agents, and
|
||||||
`meta.rs` keeps the one remaining commit to the legacy `meta/` git repo, the
|
`meta.rs` removes the permission seed files once the swarm has them, and
|
||||||
removal of the permission seed files.
|
`migrate.rs` deletes the legacy `meta/` repo's leftover `.git`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
//! Startup convergence. Three phases, all idempotent and unguarded:
|
//! 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
|
//! boot on purpose — each one is a no-op once its state is already
|
||||||
//! correct.
|
//! correct.
|
||||||
//!
|
//!
|
||||||
|
|
@ -68,6 +69,12 @@ pub async fn run() -> Result<()> {
|
||||||
migrate_harness_files(name);
|
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.
|
// Phase 1 + 2: per-agent applied + proposed.
|
||||||
tracing::debug!("migration: phase 1+2 (applied + proposed repos)");
|
tracing::debug!("migration: phase 1+2 (applied + proposed repos)");
|
||||||
// An archetype agent has neither repo, and seeding one here would hand it
|
// 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");
|
tracing::warn!(%name, error = ?e, "migration: applied repo rewrite failed");
|
||||||
}
|
}
|
||||||
let proposed_dir = Coordinator::agent_proposed_dir(name);
|
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());
|
let proposed = lifecycle::setup_proposed(&proposed_dir, name.as_str());
|
||||||
match tokio::time::timeout(GIT_TIMEOUT, proposed).await {
|
match tokio::time::timeout(GIT_TIMEOUT, proposed).await {
|
||||||
Ok(Err(e)) => tracing::warn!(%name, error = ?e, "migration: setup_proposed failed"),
|
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>> {
|
async fn enumerate_agents() -> Result<Vec<hive_types::Ident>> {
|
||||||
let containers = lifecycle::list().await?;
|
let containers = lifecycle::list().await?;
|
||||||
Ok(containers
|
Ok(containers
|
||||||
|
|
@ -248,6 +287,67 @@ async fn raw_git(dir: &Path, args: &[&str]) -> Result<()> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[tokio::test]
|
||||||
async fn unreadable_flake_is_left_untouched() {
|
async fn unreadable_flake_is_left_untouched() {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue