use super::{
MARKER, Owned, Pointer, Secrets, api_credential_key, conflict, credential_conflict, identity,
optional, read,
};
use crate::core::error::Result;
use crate::napcat::runtime::runtime_config::Bundle;
use crate::storage::{
credentials::Credentials,
references::References,
state::{Installation, Instance, Transaction, reject_links},
};
use std::time::Instant;
pub struct ControlMaterial {
endpoint: String,
token: String,
pub(crate) directory: std::path::PathBuf,
pub(crate) plan: super::Plan,
pub(crate) bundle: Bundle,
}
impl ControlMaterial {
pub fn endpoint(&self) -> &str {
&self.endpoint
}
pub fn token(&self) -> &str {
&self.token
}
}
pub fn recover_control(
tx: &Transaction<'_>,
refs: &References,
credentials: &impl Credentials,
instance: &Instance,
installation: &Installation,
deadline: Instant,
) -> Result<ControlMaterial> {
crate::core::deadline::check(deadline)?;
if tx
.registry
.instances
.iter()
.filter(|i| i.id == instance.id)
.count()
!= 1
|| !tx.registry.instances.iter().any(|i| i == instance)
|| tx
.registry
.installations
.iter()
.filter(|i| i.id == installation.id)
.count()
!= 1
|| !tx.registry.installations.iter().any(|i| i == installation)
|| instance.installation_id != installation.id
|| !matches!(
instance.state.as_str(),
"starting" | "login_required" | "ready"
)
{
return Err(conflict());
}
let id = identity(&instance.id, &instance.run_id)?;
let base = tx.store().root().canonicalize().map_err(|_| conflict())?;
let directory = base.join("runtimes").join(&id);
let stage = base.join("runtimes").join(format!("{id}.stage"));
let journal = base.join("runtime-preparations").join(format!("{id}.json"));
for path in [&directory, &stage, &journal] {
reject_links(path)?;
}
if stage.try_exists().map_err(|_| conflict())? || !directory.is_dir() {
return Err(conflict());
}
let pointer: Pointer = crate::storage::config::read_json(&journal, 4096)?;
let owned: Owned = refs.get("r1", &pointer.reference)?;
let version = crate::napcat::install::catalog::lookup(&installation.version)?.version;
if pointer.schema_version != 1
|| owned.schema_version != 1
|| owned.template_version != 1
|| owned.directory != directory
|| owned.installation_id != installation.id
|| owned.installation_digest != installation.digest
|| owned.plan.instance_id != instance.id
|| owned.plan.run_id != instance.run_id
|| owned.plan.version != version
{
return Err(conflict());
}
owned.plan.ports.validate()?;
if instance.endpoint != format!("http://127.0.0.1:{}/", owned.plan.ports.http)
|| instance.ws_endpoint.as_deref()
!= Some(format!("ws://127.0.0.1:{}/event", owned.plan.ports.events).as_str())
|| instance.credential_key != api_credential_key(&instance.id, &instance.run_id)?
{
return Err(conflict());
}
let marker: Pointer =
serde_json::from_slice(&read(&directory.join(MARKER))?).map_err(|_| conflict())?;
if marker.schema_version != 1 || marker.reference != pointer.reference {
return Err(conflict());
}
let raw = optional(credentials, &format!("runtime-bundle-{id}"), deadline)?
.ok_or_else(credential_conflict)?;
if raw.len() > 2048 {
return Err(credential_conflict());
}
let secrets: Secrets = serde_json::from_str(&raw).map_err(|_| credential_conflict())?;
if secrets.schema_version != 1 || secrets.reference != pointer.reference {
return Err(credential_conflict());
}
let bundle = Bundle::restore(
version,
owned.plan.ports,
secrets.api,
secrets.control,
secrets.jwt,
)?;
if optional(credentials, &instance.credential_key, deadline)?.as_deref()
!= Some(bundle.api_token())
{
return Err(credential_conflict());
}
crate::core::deadline::check(deadline)?;
Ok(ControlMaterial {
endpoint: format!("http://127.0.0.1:{}/", owned.plan.ports.control),
token: bundle.control_token().into(),
directory,
plan: owned.plan,
bundle,
})
}