feat: replace linked worktrees with runtime clones
This commit is contained in:
@@ -624,7 +624,7 @@ CREATE TABLE workdir_create_operations (
|
||||
state TEXT NOT NULL CHECK (state IN ('pending', 'succeeded', 'failed')),
|
||||
failure TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL, source_kind TEXT, source_uri TEXT, source_revision INTEGER, source_fingerprint TEXT, credential_id TEXT, credential_revision INTEGER, host_trust_id TEXT, host_trust_revision INTEGER, repository_access_mode TEXT, cache_generation INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL, source_kind TEXT, source_uri TEXT, source_revision INTEGER, source_fingerprint TEXT, credential_id TEXT, credential_revision INTEGER, host_trust_id TEXT, host_trust_revision INTEGER, repository_access_mode TEXT,
|
||||
PRIMARY KEY (workspace_id, operation_id),
|
||||
UNIQUE (workspace_id, working_directory_id)
|
||||
);
|
||||
|
||||
@@ -10910,7 +10910,6 @@ async fn create_workspace_working_directory(
|
||||
host_trust_id: None,
|
||||
host_trust_revision: None,
|
||||
repository_access_mode: None,
|
||||
cache_generation: 0,
|
||||
working_directory_id: next_backend_workdir_id(&request.repository_key),
|
||||
state: "pending".to_string(),
|
||||
failure: None,
|
||||
@@ -14216,7 +14215,7 @@ fn working_directory_request_from_repository(
|
||||
})
|
||||
.or_else(|| Some(RuntimeRepositorySelector::from("HEAD"))),
|
||||
},
|
||||
materializer: MaterializerKind::RuntimeGitCache,
|
||||
materializer: MaterializerKind::RuntimeGitClone,
|
||||
backend_workdir_id: None,
|
||||
materialization: None,
|
||||
}
|
||||
@@ -17381,9 +17380,9 @@ fn runtime_workdir_summary_from_record(
|
||||
current_ref: record.current_ref.clone(),
|
||||
current_tree: record.current_tree.clone(),
|
||||
observed_at_epoch_seconds: record.observed_at_epoch_seconds,
|
||||
materializer_kind: MaterializerKind::RuntimeGitCache,
|
||||
materializer_kind: MaterializerKind::RuntimeGitClone,
|
||||
cleanup_target: Some(worker_runtime::catalog::WorkingDirectoryCleanupTarget {
|
||||
kind: "runtime_git_cache_worktree".to_string(),
|
||||
kind: "runtime_git_clone".to_string(),
|
||||
working_directory_id: record.workdir_id.clone(),
|
||||
repository_id: record.repository_id.clone(),
|
||||
}),
|
||||
@@ -17416,9 +17415,9 @@ fn workdir_summary_from_record(
|
||||
current_ref: record.current_ref.clone(),
|
||||
current_tree: record.current_tree.clone(),
|
||||
observed_at_epoch_seconds: record.observed_at_epoch_seconds,
|
||||
materializer_kind: MaterializerKind::RuntimeGitCache,
|
||||
materializer_kind: MaterializerKind::RuntimeGitClone,
|
||||
cleanup_target: Some(WorkingDirectoryCleanupTarget {
|
||||
kind: "runtime_git_cache_worktree".to_string(),
|
||||
kind: "runtime_git_clone".to_string(),
|
||||
working_directory_id: record.workdir_id.clone(),
|
||||
repository_key: repository_key.to_string(),
|
||||
}),
|
||||
@@ -17647,7 +17646,6 @@ fn authorize_repository_materialization_operation(
|
||||
operation_id: operation.operation_id.clone(),
|
||||
config_revision: operation.config_revision,
|
||||
config_projection_digest: operation.config_projection_digest.clone(),
|
||||
cache_generation: operation.cache_generation,
|
||||
ssh: Some(RepositorySshMaterializationAccess {
|
||||
credential_candidates: leases
|
||||
.into_iter()
|
||||
@@ -17715,7 +17713,6 @@ fn authorize_repository_materialization_operation(
|
||||
workspace_api::RepositoryAccessMode::ReadOnly => "read_only",
|
||||
workspace_api::RepositoryAccessMode::ReadWrite => "read_write",
|
||||
},
|
||||
context.cache_generation,
|
||||
&now_registry_timestamp(),
|
||||
)?;
|
||||
context
|
||||
@@ -17727,7 +17724,6 @@ fn authorize_repository_materialization_operation(
|
||||
operation_id: operation.operation_id.clone(),
|
||||
config_revision: operation.config_revision,
|
||||
config_projection_digest: operation.config_projection_digest.clone(),
|
||||
cache_generation: operation.cache_generation,
|
||||
ssh: None,
|
||||
}
|
||||
};
|
||||
@@ -17870,7 +17866,6 @@ fn authorize_repository_materialization(
|
||||
operation_id: operation_id.to_string(),
|
||||
config_revision: projection.config_revision,
|
||||
config_projection_digest: projection.projection_digest.clone(),
|
||||
cache_generation: 0,
|
||||
ssh,
|
||||
});
|
||||
Ok(())
|
||||
@@ -17935,7 +17930,7 @@ fn working_directory_request_for_browser(
|
||||
source_fingerprint: repository.source_fingerprint.clone(),
|
||||
selector: selector.map(RuntimeRepositorySelector),
|
||||
},
|
||||
materializer: MaterializerKind::RuntimeGitCache,
|
||||
materializer: MaterializerKind::RuntimeGitClone,
|
||||
backend_workdir_id: None,
|
||||
materialization: None,
|
||||
})
|
||||
@@ -18707,7 +18702,7 @@ mod tests {
|
||||
current_ref: Some("abc123".to_string()),
|
||||
current_tree: None,
|
||||
observed_at_epoch_seconds: Some(1_767_225_600),
|
||||
materializer_kind: workspace_api::WorkingDirectoryMaterializerKind::RuntimeGitCache,
|
||||
materializer_kind: workspace_api::WorkingDirectoryMaterializerKind::RuntimeGitClone,
|
||||
cleanup_target: None,
|
||||
status: worker_runtime::catalog::WorkingDirectoryStatusKind::Active,
|
||||
cleanliness: Some("clean".to_string()),
|
||||
@@ -19758,7 +19753,6 @@ mod tests {
|
||||
operation_id: "operation-1".to_string(),
|
||||
config_revision: 1,
|
||||
config_projection_digest: "sha256:projection".to_string(),
|
||||
cache_generation: 0,
|
||||
ssh: Some(
|
||||
worker_runtime::catalog::RepositorySshMaterializationAccess {
|
||||
credential_candidates: vec![
|
||||
@@ -21096,7 +21090,7 @@ mod tests {
|
||||
worker_runtime::execution::WorkerExecutionContext,
|
||||
>,
|
||||
>,
|
||||
materializer: worker_runtime::working_directory::RuntimeGitCacheMaterializer,
|
||||
materializer: worker_runtime::working_directory::RuntimeGitMaterializer,
|
||||
spawn_failure: std::sync::Mutex<Option<String>>,
|
||||
input_failure: std::sync::Mutex<Option<String>>,
|
||||
inputs: std::sync::Mutex<Vec<(worker_runtime::identity::WorkerRef, String)>>,
|
||||
@@ -21116,7 +21110,7 @@ mod tests {
|
||||
);
|
||||
Self {
|
||||
contexts: std::sync::Mutex::new(std::collections::HashMap::new()),
|
||||
materializer: worker_runtime::working_directory::RuntimeGitCacheMaterializer::new(
|
||||
materializer: worker_runtime::working_directory::RuntimeGitMaterializer::new(
|
||||
std::env::temp_dir().join(unique),
|
||||
),
|
||||
spawn_failure: std::sync::Mutex::new(None),
|
||||
@@ -28115,7 +28109,6 @@ mod tests {
|
||||
host_trust_id: None,
|
||||
host_trust_revision: None,
|
||||
repository_access_mode: None,
|
||||
cache_generation: 0,
|
||||
working_directory_id: "workdir-provider-rejection".to_string(),
|
||||
state: "pending".to_string(),
|
||||
failure: None,
|
||||
@@ -30989,7 +30982,7 @@ VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3);
|
||||
current_ref: Some("abc123".to_string()),
|
||||
current_tree: Some("tree123".to_string()),
|
||||
observed_at_epoch_seconds: Some(1_777_777_777),
|
||||
materializer_kind: MaterializerKind::LocalGitWorktree,
|
||||
materializer_kind: MaterializerKind::RuntimeGitClone,
|
||||
cleanup_target: None,
|
||||
status: WorkingDirectoryStatusKind::Active,
|
||||
cleanliness: Some("clean".to_string()),
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::workspace_deletion::WorkspaceDeletionStore;
|
||||
use crate::{Error, Result};
|
||||
|
||||
const OLDEST_SCHEMA_VERSION: i64 = 50;
|
||||
const LATEST_SCHEMA_VERSION: i64 = 57;
|
||||
const LATEST_SCHEMA_VERSION: i64 = 58;
|
||||
const SCHEMA_BASELINE_NAME: &str = "workspace schema baseline";
|
||||
const WORKSPACE_RUNTIME_BINDINGS_MIGRATION_NAME: &str = "workspace runtime bindings";
|
||||
const RUNTIME_BINDING_AUDIT_MIGRATION_NAME: &str = "workspace Runtime binding revision and audit";
|
||||
@@ -30,6 +30,8 @@ const WORKSPACE_RUNTIME_VERIFICATION_MIGRATION_NAME: &str =
|
||||
"Workspace-signed Runtime verification evidence";
|
||||
const LEGACY_EXTERNAL_RUNTIME_BINDING_CUTOVER_MIGRATION_NAME: &str =
|
||||
"convert legacy Server-issued Runtime bindings to Workspace identity";
|
||||
const REMOVE_WORKDIR_CACHE_GENERATION_MIGRATION_NAME: &str =
|
||||
"remove obsolete Workdir Repository cache generation";
|
||||
|
||||
const MIGRATIONS: &[Migration] = &[
|
||||
Migration {
|
||||
@@ -67,6 +69,11 @@ const MIGRATIONS: &[Migration] = &[
|
||||
name: LEGACY_EXTERNAL_RUNTIME_BINDING_CUTOVER_MIGRATION_NAME,
|
||||
apply: migrate_legacy_external_runtime_bindings_v56_to_v57,
|
||||
},
|
||||
Migration {
|
||||
version: 58,
|
||||
name: REMOVE_WORKDIR_CACHE_GENERATION_MIGRATION_NAME,
|
||||
apply: migrate_workdir_cache_generation_v57_to_v58,
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -566,7 +573,6 @@ pub struct WorkdirCreateOperationRecord {
|
||||
pub host_trust_id: Option<String>,
|
||||
pub host_trust_revision: Option<u64>,
|
||||
pub repository_access_mode: Option<String>,
|
||||
pub cache_generation: u64,
|
||||
pub working_directory_id: String,
|
||||
pub state: String,
|
||||
pub failure: Option<String>,
|
||||
@@ -8534,6 +8540,33 @@ fn migrate_legacy_external_runtime_bindings_v56_to_v57(conn: &Connection) -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migrate_workdir_cache_generation_v57_to_v58(conn: &Connection) -> Result<()> {
|
||||
let current = current_schema_version(conn)?;
|
||||
if current != 57 {
|
||||
return Err(Error::Store(format!(
|
||||
"expected schema version 57 before {REMOVE_WORKDIR_CACHE_GENERATION_MIGRATION_NAME} migration, found {current}"
|
||||
)));
|
||||
}
|
||||
let tx = rusqlite::Transaction::new_unchecked(conn, TransactionBehavior::Exclusive)?;
|
||||
if table_columns(&tx, "workdir_create_operations")?.contains(&"cache_generation".to_string()) {
|
||||
tx.execute(
|
||||
"ALTER TABLE workdir_create_operations DROP COLUMN cache_generation",
|
||||
[],
|
||||
)?;
|
||||
}
|
||||
if table_columns(&tx, "workdir_create_operations")?.contains(&"cache_generation".to_string()) {
|
||||
return Err(Error::Store(
|
||||
"obsolete Workdir cache generation remains after schema-58 migration".to_string(),
|
||||
));
|
||||
}
|
||||
tx.execute(
|
||||
"INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)",
|
||||
params![58_i64, REMOVE_WORKDIR_CACHE_GENERATION_MIGRATION_NAME],
|
||||
)?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_workspace_runtime_verification_schema(conn: &Connection) -> Result<()> {
|
||||
let actual = table_columns(conn, "workspace_runtime_verifications")?
|
||||
.into_iter()
|
||||
@@ -9676,6 +9709,10 @@ mod tests {
|
||||
version: 57,
|
||||
name: LEGACY_EXTERNAL_RUNTIME_BINDING_CUTOVER_MIGRATION_NAME.to_string(),
|
||||
},
|
||||
WorkspaceSchemaMigrationStep {
|
||||
version: 58,
|
||||
name: REMOVE_WORKDIR_CACHE_GENERATION_MIGRATION_NAME.to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@@ -9710,6 +9747,10 @@ mod tests {
|
||||
57,
|
||||
LEGACY_EXTERNAL_RUNTIME_BINDING_CUTOVER_MIGRATION_NAME.to_string(),
|
||||
),
|
||||
(
|
||||
58,
|
||||
REMOVE_WORKDIR_CACHE_GENERATION_MIGRATION_NAME.to_string(),
|
||||
),
|
||||
]
|
||||
);
|
||||
assert!(!table_exists(conn, "trusted_runtime_records")?);
|
||||
@@ -9780,7 +9821,7 @@ mod tests {
|
||||
.iter()
|
||||
.map(|migration| migration.version)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![52, 53, 54, 55, 56, 57]
|
||||
vec![52, 53, 54, 55, 56, 57, 58]
|
||||
);
|
||||
SqliteWorkspaceStore::migrate_database(&path).unwrap();
|
||||
let conn = Connection::open(&path).unwrap();
|
||||
@@ -9788,7 +9829,7 @@ mod tests {
|
||||
current_schema_version(&conn).unwrap(),
|
||||
LATEST_SCHEMA_VERSION
|
||||
);
|
||||
assert_eq!(workspace_schema_migration_history(&conn).unwrap().len(), 8);
|
||||
assert_eq!(workspace_schema_migration_history(&conn).unwrap().len(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -10547,6 +10588,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_v58_removes_obsolete_workdir_cache_generation() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let path = temp.path().join("server.db");
|
||||
prepare_schema_v50(&path, Some("workspace-a"));
|
||||
let conn = Connection::open(&path).unwrap();
|
||||
configure_sqlite(&conn).unwrap();
|
||||
for migration in MIGRATIONS
|
||||
.iter()
|
||||
.filter(|migration| migration.version <= 57)
|
||||
{
|
||||
(migration.apply)(&conn).unwrap();
|
||||
}
|
||||
conn.execute(
|
||||
"ALTER TABLE workdir_create_operations ADD COLUMN cache_generation INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
migrate_workdir_cache_generation_v57_to_v58(&conn).unwrap();
|
||||
|
||||
assert_eq!(current_schema_version(&conn).unwrap(), 58);
|
||||
assert!(
|
||||
!table_columns(&conn, "workdir_create_operations")
|
||||
.unwrap()
|
||||
.contains(&"cache_generation".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_binding_key_mutations_are_revisioned_idempotent_and_audited() {
|
||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||
|
||||
@@ -172,7 +172,6 @@ impl SqliteWorkspaceStore {
|
||||
host_trust_id: &str,
|
||||
host_trust_revision: u64,
|
||||
repository_access_mode: &str,
|
||||
cache_generation: u64,
|
||||
now: &str,
|
||||
) -> Result<WorkdirCreateOperationRecord> {
|
||||
self.with_conn_mut(|conn| {
|
||||
@@ -194,7 +193,6 @@ impl SqliteWorkspaceStore {
|
||||
|| operation.host_trust_revision != Some(host_trust_revision)
|
||||
|| operation.repository_access_mode.as_deref()
|
||||
!= Some(repository_access_mode)
|
||||
|| operation.cache_generation != cache_generation
|
||||
{
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"Workdir create operation `{operation_id}` Repository access evidence changed"
|
||||
@@ -206,8 +204,7 @@ impl SqliteWorkspaceStore {
|
||||
r#"UPDATE workdir_create_operations
|
||||
SET credential_id = ?4, credential_revision = ?5,
|
||||
host_trust_id = ?6, host_trust_revision = ?7,
|
||||
repository_access_mode = ?8, cache_generation = ?9,
|
||||
updated_at = ?10
|
||||
repository_access_mode = ?8, updated_at = ?9
|
||||
WHERE workspace_id = ?1 AND operation_id = ?2
|
||||
AND request_fingerprint = ?3 AND credential_id IS NULL"#,
|
||||
params![
|
||||
@@ -223,9 +220,6 @@ impl SqliteWorkspaceStore {
|
||||
"host-trust revision is out of range".to_string()
|
||||
))?,
|
||||
repository_access_mode,
|
||||
i64::try_from(cache_generation).map_err(|_| Error::InvalidInput(
|
||||
"cache generation is out of range".to_string()
|
||||
))?,
|
||||
now,
|
||||
],
|
||||
)?;
|
||||
@@ -294,7 +288,7 @@ fn read_workdir_create_operation(
|
||||
config_projection_digest, source_kind, source_uri, source_revision,
|
||||
source_fingerprint, credential_id, credential_revision,
|
||||
host_trust_id, host_trust_revision, repository_access_mode,
|
||||
cache_generation, working_directory_id, state, failure,
|
||||
working_directory_id, state, failure,
|
||||
created_at, updated_at
|
||||
FROM workdir_create_operations
|
||||
WHERE workspace_id = ?1 AND operation_id = ?2"#,
|
||||
@@ -319,12 +313,11 @@ fn read_workdir_create_operation(
|
||||
host_trust_id: row.get(15)?,
|
||||
host_trust_revision: row.get::<_, Option<i64>>(16)?.map(|value| value as u64),
|
||||
repository_access_mode: row.get(17)?,
|
||||
cache_generation: row.get::<_, i64>(18)? as u64,
|
||||
working_directory_id: row.get(19)?,
|
||||
state: row.get(20)?,
|
||||
failure: row.get(21)?,
|
||||
created_at: row.get(22)?,
|
||||
updated_at: row.get(23)?,
|
||||
working_directory_id: row.get(18)?,
|
||||
state: row.get(19)?,
|
||||
failure: row.get(20)?,
|
||||
created_at: row.get(21)?,
|
||||
updated_at: row.get(22)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -410,7 +403,6 @@ mod tests {
|
||||
host_trust_id: None,
|
||||
host_trust_revision: None,
|
||||
repository_access_mode: None,
|
||||
cache_generation: 0,
|
||||
working_directory_id: "wd-1".to_string(),
|
||||
state: "pending".to_string(),
|
||||
failure: None,
|
||||
@@ -431,14 +423,12 @@ mod tests {
|
||||
"trust-1",
|
||||
5,
|
||||
"read_only",
|
||||
2,
|
||||
"2026-08-24T00:00:01Z",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bound.credential_id.as_deref(), Some("credential-1"));
|
||||
assert_eq!(bound.credential_revision, Some(3));
|
||||
assert_eq!(bound.host_trust_revision, Some(5));
|
||||
assert_eq!(bound.cache_generation, 2);
|
||||
assert!(
|
||||
store
|
||||
.bind_workdir_create_repository_access(
|
||||
@@ -450,7 +440,6 @@ mod tests {
|
||||
"trust-1",
|
||||
5,
|
||||
"read_only",
|
||||
2,
|
||||
"2026-08-24T00:00:02Z",
|
||||
)
|
||||
.is_err()
|
||||
|
||||
@@ -1096,7 +1096,6 @@ mod tests {
|
||||
host_trust_id: None,
|
||||
host_trust_revision: None,
|
||||
repository_access_mode: None,
|
||||
cache_generation: 0,
|
||||
working_directory_id: "workdir-a".to_string(),
|
||||
state: "pending".to_string(),
|
||||
failure: None,
|
||||
|
||||
Reference in New Issue
Block a user