mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-29 01:42:21 +00:00
feat: seed environments at install, make default deletable
Built-in environments (default, docker, daytona) are now seeded only by the installer, not re-created on every server startup. The running server loads whatever is on disk and never self-heals seeds, so an uninstalled instance has no managed environments and a run that selects an absent environment fails explicitly instead of silently resurrecting a built-in. The `default` environment is no longer protected from deletion. Deleting it removes the run fallback on purpose: a later run that selects `default` fails with "unknown environment: default", forcing an explicit choice. `local` is unchanged (reserved, in-memory). - fabro-environment: `EnvironmentStore::load_or_seed` -> `load` (no seeding); new public `seed_environments(dir)` (idempotent, preserves edits); drop the `default` delete guard and the `Protected` error. - fabro-server: startup loads without seeding; web install seeds after persistence; drop the Protected->409 mapping; test_support seeds so tests exercise an installed instance. - fabro-cli: install seeds after persisting outputs. - web: remove the "protected" tag and disabled Delete for `default`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6e9d7e4079
commit
1d61611f68
11 changed files with 176 additions and 70 deletions
|
|
@ -20,10 +20,6 @@ import {
|
|||
import { ConfirmDialog } from "../components/ui";
|
||||
import { useToast } from "../components/toast";
|
||||
|
||||
// `default` is seeded and protected from deletion by the server; reflect that
|
||||
// in the UI instead of letting the delete fail with a 409.
|
||||
const PROTECTED_ID = "default";
|
||||
|
||||
// `local` is a reserved, in-memory environment the server includes only when
|
||||
// the local sandbox provider is enabled. It has no configurable settings, so it
|
||||
// gets its own panel instead of a row in the managed environments list.
|
||||
|
|
@ -237,7 +233,6 @@ function EnvironmentRow({
|
|||
{environment.id}
|
||||
</span>
|
||||
<Badge>{environment.provider}</Badge>
|
||||
{environment.id === PROTECTED_ID ? <StatusTag>protected</StatusTag> : null}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-xs/5 text-fg-3">
|
||||
{resourcesSummary(environment)}
|
||||
|
|
@ -286,7 +281,6 @@ function RowMenu({
|
|||
disabled: boolean;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const protectedFromDelete = environment.id === PROTECTED_ID;
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block">
|
||||
<MenuButton
|
||||
|
|
@ -316,11 +310,10 @@ function RowMenu({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
disabled={disabled || protectedFromDelete}
|
||||
title={protectedFromDelete ? "The default environment is protected" : undefined}
|
||||
disabled={disabled}
|
||||
className={MENU_ITEM_DANGER_CLASS}
|
||||
>
|
||||
{protectedFromDelete ? "Protected" : "Delete"}
|
||||
Delete
|
||||
</button>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
|
|
|
|||
|
|
@ -2012,6 +2012,22 @@ async fn run_install_inner(args: &InstallArgs, ctx: &CommandContext) -> Result<(
|
|||
dev_token_for_auth_store.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Seed the built-in environments next to the settings file. The server never
|
||||
// seeds on startup, so install is the only place built-ins are written;
|
||||
// existing files are preserved, so re-running install never clobbers edits.
|
||||
let environment_dir = config_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new("."))
|
||||
.join("environments");
|
||||
if let Err(err) = fabro_environment::seed_environments(&environment_dir) {
|
||||
fabro_util::printerr!(
|
||||
printer,
|
||||
" {} Failed to seed built-in environments: {err}",
|
||||
s.yellow.apply_to("Warning:")
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(token) = dev_token_for_auth_store {
|
||||
let user_settings = UserSettingsBuilder::from_toml(&settings_toml)?;
|
||||
let target = match user_config::resolve_nondefault_server_target(
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ pub enum EnvironmentStoreError {
|
|||
expected: EnvironmentRevision,
|
||||
actual: EnvironmentRevision,
|
||||
},
|
||||
#[error("environment is protected and cannot be deleted: {id}")]
|
||||
Protected { id: EnvironmentId },
|
||||
#[error("environment is reserved and cannot be modified: {id}")]
|
||||
Reserved { id: EnvironmentId },
|
||||
#[error("environment validation failed: {source}")]
|
||||
|
|
@ -95,7 +93,6 @@ impl EnvironmentStoreError {
|
|||
Self::NotFound { .. } => "not_found",
|
||||
Self::AlreadyExists { .. } => "already_exists",
|
||||
Self::StaleRevision { .. } => "stale_revision",
|
||||
Self::Protected { .. } => "protected",
|
||||
Self::Reserved { .. } => "reserved",
|
||||
Self::Validation { .. } => "validation",
|
||||
Self::InvalidFilename { .. } => "invalid_filename",
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ mod store;
|
|||
pub use error::{EnvironmentStoreError, EnvironmentValidationError};
|
||||
pub use id::{EnvironmentId, EnvironmentRevision, EnvironmentRevisionParseError};
|
||||
pub use model::{Environment, EnvironmentDraft};
|
||||
pub use store::{EnvironmentStore, seeded_catalog_layer};
|
||||
pub use store::{EnvironmentStore, seed_environments, seeded_catalog_layer};
|
||||
|
|
|
|||
|
|
@ -14,8 +14,11 @@ use crate::{
|
|||
Environment, EnvironmentDraft, EnvironmentId, EnvironmentRevision, EnvironmentStoreError,
|
||||
};
|
||||
|
||||
/// Environments written to disk on first startup. `local` is intentionally
|
||||
/// absent: it is a reserved, in-memory environment (see [`RESERVED_LOCAL_ID`]).
|
||||
/// Built-in environments written to disk by the installer (see
|
||||
/// [`seed_environments`]). The server itself never seeds: a Fabro instance that
|
||||
/// has not been installed has no managed environments, and a run that selects
|
||||
/// an absent environment fails explicitly. `local` is intentionally absent: it
|
||||
/// is a reserved, in-memory environment (see [`RESERVED_LOCAL_ID`]).
|
||||
const SEEDS: &[(&str, &str)] = &[
|
||||
("default", DEFAULT_ENVIRONMENT_TOML),
|
||||
("docker", DOCKER_ENVIRONMENT_TOML),
|
||||
|
|
@ -130,15 +133,19 @@ fn build_catalog_layer(
|
|||
}
|
||||
|
||||
impl EnvironmentStore {
|
||||
/// Synchronously seed missing built-in environment files and load all
|
||||
/// persisted environments. The synchronous file access runs during server
|
||||
/// startup before request handling begins.
|
||||
pub fn load_or_seed(
|
||||
/// Synchronously load all persisted environments. The synchronous file
|
||||
/// access runs during server startup before request handling begins.
|
||||
///
|
||||
/// The server never seeds built-in environments; seeding is an install-time
|
||||
/// action (see [`seed_environments`]). An uninstalled instance therefore
|
||||
/// has no managed environments on disk, and the reserved `local`
|
||||
/// environment is the only entry present (when the local provider is
|
||||
/// enabled).
|
||||
pub fn load(
|
||||
dir: impl Into<PathBuf>,
|
||||
local_enabled: bool,
|
||||
) -> Result<Self, EnvironmentStoreError> {
|
||||
let dir = dir.into();
|
||||
seed_missing_environments(&dir)?;
|
||||
let mut environments = load_environments(&dir)?;
|
||||
if local_enabled {
|
||||
let local = synthetic_local_environment()?;
|
||||
|
|
@ -224,9 +231,9 @@ impl EnvironmentStore {
|
|||
id: &EnvironmentId,
|
||||
expected: &EnvironmentRevision,
|
||||
) -> Result<(), EnvironmentStoreError> {
|
||||
if id.as_str() == "default" {
|
||||
return Err(EnvironmentStoreError::Protected { id: id.clone() });
|
||||
}
|
||||
// `default` is an ordinary deletable environment. Deleting it removes the
|
||||
// run fallback, which is intentional: a run that selects `default` after
|
||||
// it is gone fails explicitly rather than silently using a built-in.
|
||||
if id.as_str() == RESERVED_LOCAL_ID {
|
||||
return Err(EnvironmentStoreError::Reserved { id: id.clone() });
|
||||
}
|
||||
|
|
@ -267,12 +274,16 @@ fn check_revision(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes the built-in environment seeds (`default`, `docker`, `daytona`) into
|
||||
/// `dir`, creating the directory if needed. Existing files are left untouched,
|
||||
/// so this is idempotent and never clobbers operator edits. Called by the
|
||||
/// installer; the running server does not seed.
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
clippy::disallowed_types,
|
||||
reason = "Environment directory seeding runs synchronously during startup before request handling."
|
||||
reason = "Install-time environment seeding runs synchronously from the installer before the server starts."
|
||||
)]
|
||||
fn seed_missing_environments(dir: &Path) -> Result<(), EnvironmentStoreError> {
|
||||
pub fn seed_environments(dir: &Path) -> Result<(), EnvironmentStoreError> {
|
||||
std::fs::create_dir_all(dir).map_err(|err| EnvironmentStoreError::io(dir, err))?;
|
||||
for (id, content) in SEEDS {
|
||||
let path = dir.join(format!("{id}.toml"));
|
||||
|
|
@ -487,27 +498,68 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn absent_directory_loads_and_seeds_built_ins() {
|
||||
async fn load_does_not_seed_built_ins() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let environment_dir = dir.path().join("environments");
|
||||
|
||||
let store = EnvironmentStore::load_or_seed(&environment_dir, true).unwrap();
|
||||
let environments = store.list();
|
||||
|
||||
// `local` is present in memory (local provider enabled) but the other
|
||||
// three are the on-disk seeds.
|
||||
// The server loads without seeding: an uninstalled instance has only the
|
||||
// reserved in-memory `local` environment, and nothing is written to disk.
|
||||
let store = EnvironmentStore::load(&environment_dir, true).unwrap();
|
||||
assert_eq!(
|
||||
environments
|
||||
store
|
||||
.list()
|
||||
.iter()
|
||||
.map(|environment| environment.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["daytona", "default", "docker", "local"]
|
||||
vec!["local"]
|
||||
);
|
||||
for id in ["default", "docker", "daytona"] {
|
||||
assert!(!environment_dir.join(format!("{id}.toml")).exists());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn seed_environments_writes_built_ins_and_load_picks_them_up() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let environment_dir = dir.path().join("environments");
|
||||
|
||||
super::seed_environments(&environment_dir).unwrap();
|
||||
for id in ["default", "docker", "daytona"] {
|
||||
assert!(environment_dir.join(format!("{id}.toml")).exists());
|
||||
}
|
||||
// `local` is reserved and in-memory; it is never written to disk.
|
||||
assert!(!environment_dir.join("local.toml").exists());
|
||||
|
||||
let store = EnvironmentStore::load(&environment_dir, true).unwrap();
|
||||
assert_eq!(
|
||||
store
|
||||
.list()
|
||||
.iter()
|
||||
.map(|environment| environment.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["daytona", "default", "docker", "local"]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn seed_environments_is_idempotent_and_preserves_edits() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let environment_dir = dir.path().join("environments");
|
||||
|
||||
super::seed_environments(&environment_dir).unwrap();
|
||||
// An operator edit to a seeded file must survive a re-seed.
|
||||
fs::write(
|
||||
environment_dir.join("default.toml"),
|
||||
"provider = \"docker\"\n[resources]\ncpu = 7\n",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
super::seed_environments(&environment_dir).unwrap();
|
||||
|
||||
let store = EnvironmentStore::load(&environment_dir, false).unwrap();
|
||||
let default = store.get(&EnvironmentId::new("default").unwrap()).unwrap();
|
||||
assert_eq!(default.settings.resources.cpu, Some(7));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -515,10 +567,10 @@ mod tests {
|
|||
let dir = tempfile::tempdir().unwrap();
|
||||
let environment_dir = dir.path().join("environments");
|
||||
|
||||
let enabled = EnvironmentStore::load_or_seed(&environment_dir, true).unwrap();
|
||||
let enabled = EnvironmentStore::load(&environment_dir, true).unwrap();
|
||||
assert!(enabled.get(&EnvironmentId::new("local").unwrap()).is_some());
|
||||
|
||||
let disabled = EnvironmentStore::load_or_seed(&environment_dir, false).unwrap();
|
||||
let disabled = EnvironmentStore::load(&environment_dir, false).unwrap();
|
||||
assert!(
|
||||
disabled
|
||||
.get(&EnvironmentId::new("local").unwrap())
|
||||
|
|
@ -539,7 +591,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let store = EnvironmentStore::load_or_seed(&environment_dir, true).unwrap();
|
||||
let store = EnvironmentStore::load(&environment_dir, true).unwrap();
|
||||
let local = store.get(&EnvironmentId::new("local").unwrap()).unwrap();
|
||||
|
||||
// The synthetic local carries no resources; the stale file was ignored.
|
||||
|
|
@ -549,7 +601,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn local_mutations_are_reserved() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = EnvironmentStore::load_or_seed(dir.path().join("environments"), true).unwrap();
|
||||
let store = EnvironmentStore::load(dir.path().join("environments"), true).unwrap();
|
||||
let local = EnvironmentId::new("local").unwrap();
|
||||
let revision = store.get(&local).unwrap().revision;
|
||||
|
||||
|
|
@ -584,7 +636,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let store = EnvironmentStore::load_or_seed(&environment_dir, true).unwrap();
|
||||
let store = EnvironmentStore::load(&environment_dir, true).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
store
|
||||
|
|
@ -592,7 +644,7 @@ mod tests {
|
|||
.iter()
|
||||
.map(|environment| environment.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["a", "daytona", "default", "docker", "local", "z"]
|
||||
vec!["a", "local", "z"]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -603,7 +655,7 @@ mod tests {
|
|||
std::fs::create_dir_all(&environment_dir).unwrap();
|
||||
std::fs::write(environment_dir.join("Bad.toml"), r#"provider = "local""#).unwrap();
|
||||
|
||||
let err = EnvironmentStore::load_or_seed(&environment_dir, true).unwrap_err();
|
||||
let err = EnvironmentStore::load(&environment_dir, true).unwrap_err();
|
||||
|
||||
assert!(matches!(err, EnvironmentStoreError::InvalidFilename { .. }));
|
||||
}
|
||||
|
|
@ -615,7 +667,7 @@ mod tests {
|
|||
std::fs::create_dir_all(&environment_dir).unwrap();
|
||||
std::fs::write(environment_dir.join("bad.toml"), r#"provider = "bogus""#).unwrap();
|
||||
|
||||
let err = EnvironmentStore::load_or_seed(&environment_dir, true).unwrap_err();
|
||||
let err = EnvironmentStore::load(&environment_dir, true).unwrap_err();
|
||||
|
||||
assert!(matches!(err, EnvironmentStoreError::Validation { .. }));
|
||||
assert!(err.to_string().contains("unknown environment provider"));
|
||||
|
|
@ -637,7 +689,7 @@ mode = "cidr_allow_list"
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let err = EnvironmentStore::load_or_seed(&environment_dir, true).unwrap_err();
|
||||
let err = EnvironmentStore::load(&environment_dir, true).unwrap_err();
|
||||
|
||||
assert!(matches!(err, EnvironmentStoreError::Validation { .. }));
|
||||
assert!(
|
||||
|
|
@ -662,7 +714,7 @@ path = "Dockerfile"
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let err = EnvironmentStore::load_or_seed(&environment_dir, true).unwrap_err();
|
||||
let err = EnvironmentStore::load(&environment_dir, true).unwrap_err();
|
||||
|
||||
assert!(matches!(err, EnvironmentStoreError::Validation { .. }));
|
||||
assert!(err.to_string().contains("Dockerfile"));
|
||||
|
|
@ -671,7 +723,11 @@ path = "Dockerfile"
|
|||
#[tokio::test]
|
||||
async fn create_conflict_is_rejected() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = EnvironmentStore::load_or_seed(dir.path().join("environments"), true).unwrap();
|
||||
let store = EnvironmentStore::load(dir.path().join("environments"), true).unwrap();
|
||||
store
|
||||
.create(draft("docker", EnvironmentProvider::Docker))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let err = store
|
||||
.create(draft("docker", EnvironmentProvider::Docker))
|
||||
|
|
@ -684,7 +740,7 @@ path = "Dockerfile"
|
|||
#[tokio::test]
|
||||
async fn create_invalid_settings_is_rejected() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = EnvironmentStore::load_or_seed(dir.path().join("environments"), true).unwrap();
|
||||
let store = EnvironmentStore::load(dir.path().join("environments"), true).unwrap();
|
||||
let mut settings = settings(EnvironmentProvider::Local);
|
||||
settings.network.mode = EnvironmentNetworkMode::Block;
|
||||
|
||||
|
|
@ -706,8 +762,11 @@ path = "Dockerfile"
|
|||
#[tokio::test]
|
||||
async fn replace_stale_revision_is_rejected() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = EnvironmentStore::load_or_seed(dir.path().join("environments"), true).unwrap();
|
||||
let current = store.get(&EnvironmentId::new("docker").unwrap()).unwrap();
|
||||
let store = EnvironmentStore::load(dir.path().join("environments"), true).unwrap();
|
||||
let current = store
|
||||
.create(draft("docker", EnvironmentProvider::Docker))
|
||||
.await
|
||||
.unwrap();
|
||||
let stale = EnvironmentRevision::from_bytes(b"stale");
|
||||
|
||||
let err = store
|
||||
|
|
@ -719,24 +778,26 @@ path = "Dockerfile"
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_delete_is_rejected() {
|
||||
async fn default_is_deletable() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = EnvironmentStore::load_or_seed(dir.path().join("environments"), true).unwrap();
|
||||
let environment_dir = dir.path().join("environments");
|
||||
super::seed_environments(&environment_dir).unwrap();
|
||||
let store = EnvironmentStore::load(&environment_dir, true).unwrap();
|
||||
let default = store.get(&EnvironmentId::new("default").unwrap()).unwrap();
|
||||
|
||||
let err = store
|
||||
.delete(&default.id, &default.revision)
|
||||
.await
|
||||
.unwrap_err();
|
||||
// `default` is an ordinary environment: deleting it succeeds and removes
|
||||
// the run fallback rather than being protected.
|
||||
store.delete(&default.id, &default.revision).await.unwrap();
|
||||
|
||||
assert!(matches!(err, EnvironmentStoreError::Protected { .. }));
|
||||
assert!(store.get(&default.id).is_none());
|
||||
assert!(!environment_dir.join("default.toml").exists());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_success_removes_file_and_memory_entry() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let environment_dir = dir.path().join("environments");
|
||||
let store = EnvironmentStore::load_or_seed(&environment_dir, true).unwrap();
|
||||
let store = EnvironmentStore::load(&environment_dir, true).unwrap();
|
||||
let created = store
|
||||
.create(draft("tmp", EnvironmentProvider::Local))
|
||||
.await
|
||||
|
|
@ -751,7 +812,7 @@ path = "Dockerfile"
|
|||
#[tokio::test]
|
||||
async fn canonical_revision_changes_when_persisted_bytes_change() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = EnvironmentStore::load_or_seed(dir.path().join("environments"), true).unwrap();
|
||||
let store = EnvironmentStore::load(dir.path().join("environments"), true).unwrap();
|
||||
let created = store
|
||||
.create(draft("rev", EnvironmentProvider::Local))
|
||||
.await
|
||||
|
|
@ -776,7 +837,7 @@ path = "Dockerfile"
|
|||
fs::write(dir.path().join("Dockerfile"), "FROM alpine\n")
|
||||
.await
|
||||
.unwrap();
|
||||
let store = EnvironmentStore::load_or_seed(dir.path().join("environments"), true).unwrap();
|
||||
let store = EnvironmentStore::load(dir.path().join("environments"), true).unwrap();
|
||||
let mut settings = settings(EnvironmentProvider::Docker);
|
||||
settings.image.dockerfile = Some(DockerfileSource::Path {
|
||||
path: "Dockerfile".to_string(),
|
||||
|
|
|
|||
|
|
@ -731,6 +731,15 @@ fn install_listen_config(bind: &Bind) -> InstallListenConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// The environments directory sits next to the active settings file, matching
|
||||
/// the server's own `environment_dir_for_active_config` derivation.
|
||||
fn install_environment_dir(config_path: &Path) -> PathBuf {
|
||||
config_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new("."))
|
||||
.join("environments")
|
||||
}
|
||||
|
||||
async fn health() -> Response {
|
||||
Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
|
|
@ -1709,6 +1718,14 @@ async fn post_install_finish(
|
|||
.into_response();
|
||||
}
|
||||
|
||||
// Seed the built-in environments next to the settings file. The server does
|
||||
// not seed on startup, so install is the only place built-ins are written;
|
||||
// existing files are preserved, so re-running install never clobbers edits.
|
||||
let environment_dir = install_environment_dir(state.config_path.as_ref());
|
||||
if let Err(err) = fabro_environment::seed_environments(&environment_dir) {
|
||||
warn!(error = %err, "failed to seed built-in environments after install");
|
||||
}
|
||||
|
||||
if let Ok(settings) = fabro_config::ServerSettingsBuilder::from_toml(&settings_toml) {
|
||||
if let Err(err) = write_artifact_store_metadata(&settings, state.storage_dir.as_ref()).await
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2327,7 +2327,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
.local
|
||||
.enabled;
|
||||
let environment_store = Arc::new(
|
||||
EnvironmentStore::load_or_seed(environment_dir, local_provider_enabled)
|
||||
EnvironmentStore::load(environment_dir, local_provider_enabled)
|
||||
.map_err(anyhow::Error::new)
|
||||
.context("load environments")?,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -236,10 +236,6 @@ impl From<EnvironmentStoreError> for ApiError {
|
|||
StatusCode::CONFLICT,
|
||||
format!("environment revision is stale: {id}"),
|
||||
),
|
||||
EnvironmentStoreError::Protected { id } => Self::new(
|
||||
StatusCode::CONFLICT,
|
||||
format!("environment is protected and cannot be deleted: {id}"),
|
||||
),
|
||||
EnvironmentStoreError::Reserved { id } => Self::new(
|
||||
StatusCode::CONFLICT,
|
||||
format!("environment is reserved and cannot be modified: {id}"),
|
||||
|
|
|
|||
|
|
@ -1222,8 +1222,10 @@ id = "missing"
|
|||
#[test]
|
||||
fn system_sandbox_provider_uses_manifest_defaults() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let environment_store = EnvironmentStore::load_or_seed(temp.path().join("environments"), true)
|
||||
.expect("environment store should seed");
|
||||
let environment_dir = temp.path().join("environments");
|
||||
fabro_environment::seed_environments(&environment_dir).expect("seed built-in environments");
|
||||
let environment_store =
|
||||
EnvironmentStore::load(&environment_dir, true).expect("environment store should load");
|
||||
let source = r#"
|
||||
_version = 1
|
||||
|
||||
|
|
@ -1241,8 +1243,8 @@ id = "daytona"
|
|||
#[test]
|
||||
fn system_sandbox_provider_defaults_when_manifest_run_settings_do_not_resolve() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let environment_store = EnvironmentStore::load_or_seed(temp.path().join("environments"), true)
|
||||
.expect("environment store should seed");
|
||||
let environment_store = EnvironmentStore::load(temp.path().join("environments"), true)
|
||||
.expect("environment store should load");
|
||||
let source = r#"
|
||||
_version = 1
|
||||
|
||||
|
|
|
|||
|
|
@ -237,6 +237,15 @@ impl TestAppStateBuilder {
|
|||
let active_config_path = self
|
||||
.active_config_path
|
||||
.unwrap_or_else(|| vault_path.with_file_name("settings.toml"));
|
||||
// Production seeds environments at install time, not on startup. Tests
|
||||
// exercise an installed instance, so seed the built-ins next to the
|
||||
// settings file before `build_app_state` loads them.
|
||||
let environment_dir = active_config_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| std::path::Path::new("."))
|
||||
.join("environments");
|
||||
fabro_environment::seed_environments(&environment_dir)
|
||||
.expect("test environments should seed");
|
||||
build_app_state(AppStateConfig {
|
||||
resolved_settings: resolved_runtime_settings_for_tests(
|
||||
self.server_settings,
|
||||
|
|
|
|||
|
|
@ -573,7 +573,7 @@ async fn dockerfile_path_over_rest_is_rejected_without_persisting_or_exposing_co
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_environment_removes_non_default_and_default_is_protected() {
|
||||
async fn delete_environment_removes_non_default_and_default_is_deletable() {
|
||||
let (app, _temp_dir, environment_dir) = environment_app();
|
||||
let created = create_environment(&app, "delete-env", "local").await;
|
||||
let revision = revision_from(&created);
|
||||
|
|
@ -608,13 +608,16 @@ async fn delete_environment_removes_non_default_and_default_is_protected() {
|
|||
)
|
||||
.await;
|
||||
|
||||
// `default` is an ordinary environment: it can be deleted, which removes the
|
||||
// run fallback. The server no longer protects it.
|
||||
let default = app
|
||||
.clone()
|
||||
.oneshot(empty_request(Method::GET, "/environments/default"))
|
||||
.await
|
||||
.expect("get default environment should respond");
|
||||
let default = response_json(default, StatusCode::OK, "GET /api/v1/environments/default").await;
|
||||
let protected = app
|
||||
let deleted = app
|
||||
.clone()
|
||||
.oneshot(request_with_if_match(
|
||||
Method::DELETE,
|
||||
"/environments/default",
|
||||
|
|
@ -624,11 +627,23 @@ async fn delete_environment_removes_non_default_and_default_is_protected() {
|
|||
.await
|
||||
.expect("delete default environment should respond");
|
||||
response_status(
|
||||
protected,
|
||||
StatusCode::CONFLICT,
|
||||
deleted,
|
||||
StatusCode::NO_CONTENT,
|
||||
"DELETE /api/v1/environments/default",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!environment_dir.join("default.toml").exists());
|
||||
let missing_default = app
|
||||
.oneshot(empty_request(Method::GET, "/environments/default"))
|
||||
.await
|
||||
.expect("get deleted default environment should respond");
|
||||
response_status(
|
||||
missing_default,
|
||||
StatusCode::NOT_FOUND,
|
||||
"GET /api/v1/environments/default after delete",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue