#[cfg(windows)]
pub(crate) mod transition;
use crate::napcat::runtime::ports::Lease;
use crate::{
core::error::{AppError, Result},
napcat::compatibility::Witness,
napcat::doctor::Probe,
napcat::install::catalog::Release,
napcat::runtime::runtime_store::{self, Plan},
napcat::runtime::startup::Inputs,
storage::references::References,
storage::state::{Installation, Instance, Store, atomic_json, private_dir, reject_links},
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::Instant;
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum Phase {
Reserved,
Registered,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct Record {
schema_version: u8,
request_id: String,
installation: Installation,
plan: Plan,
phase: Phase,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct Pointer {
schema_version: u8,
reference: String,
}
pub enum Creation {
Fresh(Reservation),
Existing { instance: Instance, plan: Plan },
}
pub struct Evidence<'a, P: Probe> {
pub release: &'a Release,
pub witnesses: &'a [Witness],
pub probe: &'a P,
}
pub struct Reservation {
plan: Plan,
_ports: Lease,
}
impl Reservation {
pub fn plan(&self) -> &Plan {
&self.plan
}
}
pub fn reserved_plan(store: &Store, refs: &References, request_id: &str) -> Result<Option<Plan>> {
Ok(load(store, refs, request_id)?.map(|record| record.plan))
}
pub struct Checkpoint {
pub plan: Plan,
pub installation_id: String,
pub installation_digest: String,
}
pub fn checkpoint(
store: &Store,
refs: &References,
request_id: &str,
) -> Result<Option<Checkpoint>> {
Ok(load(store, refs, request_id)?.map(|record| Checkpoint {
plan: record.plan,
installation_id: record.installation.id,
installation_digest: record.installation.digest,
}))
}
fn load(store: &Store, refs: &References, request_id: &str) -> Result<Option<Record>> {
let path = path(store, request_id)?;
let pointer: Pointer = match crate::storage::config::read_json(&path, 4096) {
Ok(pointer) => pointer,
Err(error) if error.code == "FILE_NOT_FOUND" => return Ok(None),
Err(error) => return Err(error),
};
if pointer.schema_version != 1 {
return Err(conflict());
}
let record: Record = refs.get("n1", &pointer.reference)?;
if record.schema_version != 1 || record.request_id != request_id {
return Err(conflict());
}
record.plan.ports.validate()?;
for key in [&record.plan.instance_id, &record.plan.run_id] {
crate::core::domain::request_key(key).map_err(|_| conflict())?;
}
crate::core::domain::request_key(&record.installation.id).map_err(|_| conflict())?;
if record.installation.digest.len() != 64
|| !record
.installation
.digest
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
{
return Err(conflict());
}
Ok(Some(record))
}
impl Inputs {
pub fn reserve_instance<P: Probe>(
&self,
store: &Store,
refs: &References,
request_id: &str,
evidence: &Evidence<'_, P>,
deadline: Instant,
) -> Result<Creation> {
let Evidence {
release,
witnesses,
probe,
} = evidence;
crate::core::deadline::check(deadline)?;
crate::core::domain::request_key(request_id).map_err(|_| AppError::invalid())?;
let mut tx = store.transaction(deadline)?;
crate::requests::keys::guard_start(store, request_id)?;
let installation =
self.with_revalidated(&tx, release, witnesses, *probe, deadline, |_, fresh| {
Ok(fresh.installation().clone())
})?;
let parent = store.root().join("instance-creations");
let path = parent.join(format!("{:x}.json", Sha256::digest(request_id.as_bytes())));
reject_links(&path)?;
let mut record = match crate::storage::config::read_json::<Pointer>(&path, 4096) {
Ok(pointer) => {
if pointer.schema_version != 1 {
return Err(conflict());
}
let record: Record = refs.get("n1", &pointer.reference)?;
if record.schema_version != 1
|| record.request_id != request_id
|| record.installation != installation
|| record.plan.version != release.version
{
return Err(conflict());
}
record.plan.ports.validate()?;
for key in [&record.plan.instance_id, &record.plan.run_id] {
crate::core::domain::request_key(key).map_err(|_| conflict())?;
}
record
}
Err(error) if error.code == "FILE_NOT_FOUND" => {
#[cfg(windows)]
if super::generation::has_origin_for_request(store, request_id)? {
return Err(conflict());
}
let ports = Lease::dynamic(deadline)?;
let plan = Plan {
instance_id: random_id("instance")?,
run_id: random_id("run")?,
version: release.version.into(),
ports: ports.ports(),
};
if tx
.registry
.instances
.iter()
.any(|i| i.id == plan.instance_id || i.run_id == plan.run_id)
{
return Err(conflict());
}
let mut record = Record {
schema_version: 1,
request_id: request_id.into(),
installation,
plan,
phase: Phase::Reserved,
};
private_dir(&parent)?;
save(&path, refs, &record)?; tx.registry.instances.push(instance(&record)?);
crate::core::deadline::check(deadline)?;
tx.persist()?;
record.phase = Phase::Registered;
save(&path, refs, &record)?;
return Ok(Creation::Fresh(Reservation {
plan: record.plan,
_ports: ports,
}));
}
Err(error) => return Err(error),
};
if let Some(current) = tx
.registry
.instances
.iter()
.find(|i| i.id == record.plan.instance_id)
{
let expected = instance(&record)?;
if current.run_id != expected.run_id
|| current.installation_id != expected.installation_id
|| current.endpoint != expected.endpoint
|| current.ws_endpoint != expected.ws_endpoint
|| current.credential_key != expected.credential_key
{
return Err(conflict());
}
if record.phase == Phase::Reserved {
record.phase = Phase::Registered;
save(&path, refs, &record)?;
}
return Ok(Creation::Existing {
instance: current.clone(),
plan: record.plan,
});
}
if record.phase == Phase::Registered {
return Err(AppError::new(
"INSTANCE_REMOVED",
"The instance reserved by this creation key is no longer registered; the key cannot create a replacement.",
4,
));
}
let ports = Lease::acquire(record.plan.ports, deadline)?;
tx.registry.instances.push(instance(&record)?);
crate::core::deadline::check(deadline)?;
tx.persist()?;
record.phase = Phase::Registered;
save(&path, refs, &record)?;
Ok(Creation::Fresh(Reservation {
plan: record.plan,
_ports: ports,
}))
}
}
fn instance(record: &Record) -> Result<Instance> {
Ok(Instance {
id: record.plan.instance_id.clone(),
run_id: record.plan.run_id.clone(),
installation_id: record.installation.id.clone(),
endpoint: format!("http://127.0.0.1:{}/", record.plan.ports.http),
ws_endpoint: Some(format!("ws://127.0.0.1:{}/event", record.plan.ports.events)),
credential_key: runtime_store::api_credential_key(
&record.plan.instance_id,
&record.plan.run_id,
)?,
account: None,
state: "preparing".into(),
process_id: None,
process_started: None,
})
}
fn path(store: &Store, request_id: &str) -> Result<std::path::PathBuf> {
crate::core::domain::request_key(request_id).map_err(|_| AppError::invalid())?;
let path = store
.root()
.join("instance-creations")
.join(format!("{:x}.json", Sha256::digest(request_id.as_bytes())));
reject_links(&path)?;
Ok(path)
}
fn save(path: &std::path::Path, refs: &References, record: &Record) -> Result<()> {
reject_links(path)?;
atomic_json(
path,
&Pointer {
schema_version: 1,
reference: refs.issue("n1", record)?,
},
)
}
fn random_id(prefix: &str) -> Result<String> {
let mut bytes = [0u8; 16];
getrandom::fill(&mut bytes).map_err(|_| AppError::storage())?;
Ok(format!(
"{prefix}-{}",
bytes.iter().map(|b| format!("{b:02x}")).collect::<String>()
))
}
fn conflict() -> AppError {
AppError::new(
"INSTANCE_CREATION_CONFLICT",
"Creation key, installation or reserved instance identity does not match; no replacement was created.",
4,
)
}
#[cfg(test)]
mod tests;