use serde::Deserialize;
use crate::apply::{
ApplyCompute, ApplyFunction, ApplyManifest, ApplySite, CURRENT_MANIFEST_VERSION, Error, Result,
};
pub fn peek_version(text: &str) -> Result<Option<u32>> {
#[derive(Deserialize, Default)]
struct VersionPeek {
#[serde(default)]
version: Option<u32>,
}
let peek: VersionPeek = crate::config::ron_options().from_str(text)?;
Ok(peek.version)
}
pub fn migrate_to_current(text: &str, from: u32) -> Result<ApplyManifest> {
match from {
1 => migrate_v1_to_v2(text),
other => Err(Error::Migration {
from: other,
reason: format!(
"no migration is registered from version {other} to the current version \
{CURRENT_MANIFEST_VERSION}"
),
}),
}
}
#[derive(Deserialize)]
#[serde(default, deny_unknown_fields)]
struct V1ApplyManifest {
#[serde(default)]
#[allow(dead_code)]
version: Option<u32>,
#[serde(default)]
project: Option<String>,
#[serde(default)]
sites: Vec<ApplySite>,
#[serde(default)]
functions: Vec<ApplyFunction>,
#[serde(default)]
compute: Vec<V1ApplyCompute>,
#[serde(default)]
tenancy: Option<boatramp_core::tenancy::TenancySchema>,
}
impl Default for V1ApplyManifest {
fn default() -> Self {
Self {
version: Some(1),
project: None,
sites: Vec::new(),
functions: Vec::new(),
compute: Vec::new(),
tenancy: None,
}
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct V1ApplyCompute {
name: String,
spec: serde_json::Value,
}
fn migrate_v1_to_v2(text: &str) -> Result<ApplyManifest> {
let v1: V1ApplyManifest = crate::config::ron_options()
.from_str(text)
.map_err(|source| Error::Migration {
from: 1,
reason: format!("this does not parse as a v1 manifest either: {source}"),
})?;
let mut compute = Vec::with_capacity(v1.compute.len());
for c in v1.compute {
let req: boatramp_core::compute::PutComputeRequest = serde_json::from_value(c.spec)
.map_err(|source| Error::Migration {
from: 1,
reason: format!(
"compute `{}`: its v1 `spec` is not a valid PutComputeRequest \
(spec/replicas/placement): {source}",
c.name
),
})?;
warn_if_db_shaped(&c.name, &req.spec);
compute.push(ApplyCompute {
name: c.name,
spec: req.spec,
replicas: req.replicas,
placement: req.placement,
});
}
Ok(ApplyManifest {
version: None,
project: v1.project,
sites: v1.sites,
functions: v1.functions,
compute,
tenancy: v1.tenancy,
databases: Vec::new(),
})
}
fn warn_if_db_shaped(name: &str, spec: &boatramp_core::compute::ComputeSpec) {
let boatramp_core::compute::RootSource::Image(image) = &spec.root else {
return;
};
const DB_HINTS: &[&str] = &[
"postgres",
"pgvector",
"mysql",
"mariadb",
"redis",
"mongo",
"clickhouse",
"cockroach",
"timescale",
];
let lower = image.to_ascii_lowercase();
if DB_HINTS.iter().any(|hint| lower.contains(hint)) {
eprintln!(
" ⚠ compute `{name}` runs a database-shaped image (`{image}`). v0.6.0 adds \
a managed `databases:` block that provisions the engine and mints + seals \
its credential for you — consider declaring it there instead of as a raw \
compute workload. (This migration keeps it as compute; no change is \
required.)"
);
}
}
pub fn render_manifest(manifest: &ApplyManifest) -> Result<String> {
let pretty = ron::ser::PrettyConfig::new().indentor(" ".to_string());
ron::ser::to_string_pretty(manifest, pretty).map_err(|source| Error::Migration {
from: 0,
reason: format!("could not render the upgraded manifest as RON: {source}"),
})
}