fabro/lib/crates/fabro-environment/src/error.rs
Bryan Helmkamp bec4b90ad3
Move environments to SQLite storage (#539)
## Summary

Move server-managed environments from sibling TOML files into SQLite,
matching the storage model already used by variables and secrets.

This adds:
- an `environments` SQLite table with DB-level validation for IDs,
revisions, providers, network modes, booleans, and JSON fields
- a SQLite-backed `EnvironmentStore` with cached synchronous reads,
transactional create/replace/delete, synthetic unpersisted `local`, and
`default` as an ordinary seeded row users can delete
- one-time legacy import from `environments/*.toml` next to the active
server `settings.toml`, including relative Dockerfile path inlining and
backup rename to `environments.imported-<timestamp>.bak`
- install/test/CLI seeding of `default` directly into SQLite instead of
writing `environments/default.toml`
- docs updates for API/SQLite-managed server environments and legacy
import behavior

The REST API shape is unchanged; path Dockerfile sources remain rejected
over the environments API.

## Testing

- `cargo nextest run -p fabro-db -p fabro-environment` - 15 passed
- `cargo nextest run -p fabro-server --features test-support
environments` - 16 passed
- `cargo nextest run -p fabro-server --features test-support install` -
60 passed
- `cargo nextest run -p fabro-server --features test-support
create_run_rejects_disabled_sandbox_provider` - 1 passed
- `cargo nextest run -p fabro-server --features test-support
system_sandbox_provider` - 2 passed
- `cargo nextest run -p fabro-cli install` - 132 passed
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
2026-07-01 10:31:58 -04:00

136 lines
4.2 KiB
Rust

use std::path::PathBuf;
use toml::de::Error as TomlDeError;
use toml::ser::Error as TomlSerError;
use crate::{EnvironmentId, EnvironmentRevision, EnvironmentRevisionParseError};
#[derive(Debug, thiserror::Error)]
pub enum EnvironmentValidationError {
#[error("environment id {value:?} must match [a-z0-9][a-z0-9-]{{0,62}}")]
InvalidEnvironmentId { value: String },
#[error("environment settings are invalid: {}", .errors.join("; "))]
InvalidSettings { errors: Vec<String> },
#[error("failed to read Dockerfile referenced by environment at {path:?}")]
DockerfileRead {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error(
"Dockerfile path sources are not supported for stored environments; use inline Dockerfile content"
)]
DockerfilePathUnsupported,
}
#[derive(Debug, thiserror::Error)]
pub enum EnvironmentStoreError {
#[error("environment not found: {id}")]
NotFound { id: EnvironmentId },
#[error("environment already exists: {id}")]
AlreadyExists { id: EnvironmentId },
#[error("environment revision is stale for {id}: expected {expected}, actual {actual}")]
StaleRevision {
id: EnvironmentId,
expected: EnvironmentRevision,
actual: EnvironmentRevision,
},
#[error("environment is reserved and cannot be modified: {id}")]
Reserved { id: EnvironmentId },
#[error("environment validation failed: {source}")]
Validation {
#[from]
source: EnvironmentValidationError,
},
#[error("invalid environment filename at {path:?}")]
InvalidFilename { path: PathBuf, reason: String },
#[error("failed to parse environment TOML at {path:?}")]
Parse {
path: PathBuf,
#[source]
source: TomlDeError,
},
#[error("invalid persisted environment revision for {id}")]
InvalidRevision {
id: EnvironmentId,
#[source]
source: EnvironmentRevisionParseError,
},
#[error("environment TOML at {path:?} is not UTF-8")]
InvalidUtf8 {
path: PathBuf,
#[source]
source: std::str::Utf8Error,
},
#[error("failed to serialize environment TOML")]
Serialize {
#[from]
source: TomlSerError,
},
#[error("I/O error at {path:?}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to encode environment JSON for {field}")]
JsonEncode {
field: &'static str,
#[source]
source: serde_json::Error,
},
#[error("failed to decode environment JSON for {field}")]
JsonDecode {
field: &'static str,
#[source]
source: serde_json::Error,
},
#[error("database error")]
Db {
#[from]
source: sqlx::Error,
},
#[error("environment row count {count} exceeds SQLite integer range")]
RowCountOverflow { count: usize },
}
impl EnvironmentStoreError {
pub(crate) fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
Self::Io {
path: path.into(),
source,
}
}
pub(crate) fn parse(path: impl Into<PathBuf>, source: TomlDeError) -> Self {
Self::Parse {
path: path.into(),
source,
}
}
pub(crate) fn invalid_utf8(path: impl Into<PathBuf>, source: std::str::Utf8Error) -> Self {
Self::InvalidUtf8 {
path: path.into(),
source,
}
}
#[must_use]
pub fn kind(&self) -> &'static str {
match self {
Self::NotFound { .. } => "not_found",
Self::AlreadyExists { .. } => "already_exists",
Self::StaleRevision { .. } => "stale_revision",
Self::Reserved { .. } => "reserved",
Self::Validation { .. } => "validation",
Self::InvalidFilename { .. } => "invalid_filename",
Self::Parse { .. } | Self::InvalidUtf8 { .. } | Self::InvalidRevision { .. } => "parse",
Self::Serialize { .. } => "serialize",
Self::JsonEncode { .. } | Self::JsonDecode { .. } => "json",
Self::Db { .. } => "db",
Self::RowCountOverflow { .. } => "row_count_overflow",
Self::Io { .. } => "io",
}
}
}