Files
start-os/core/src/service/effects/dependency.rs

426 lines
13 KiB
Rust

use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::str::FromStr;
use clap::builder::ValueParserFactory;
use exver::VersionRange;
use rust_i18n::t;
use crate::db::model::package::{
CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference,
TaskEntry,
};
use crate::disk::mount::filesystem::bind::{Bind, FileType};
use crate::disk::mount::filesystem::idmapped::{IdMap, IdMapped};
use crate::disk::mount::filesystem::{FileSystem, MountType};
use crate::disk::mount::util::{is_mountpoint, unmount};
use crate::s9pk::manifest::Manifest;
use crate::service::effects::callbacks::CallbackHandler;
use crate::service::effects::prelude::*;
use crate::service::rpc::CallbackId;
use crate::status::health_check::NamedHealthCheckResult;
use crate::util::{FromStrParser, VersionString};
use crate::volume::data_dir;
use crate::{DATA_DIR, HealthCheckId, PackageId, ReplayId, VolumeId};
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct MountTarget {
package_id: PackageId,
volume_id: VolumeId,
subpath: Option<PathBuf>,
readonly: bool,
#[serde(skip_deserializing)]
#[ts(skip)]
filetype: FileType,
#[serde(default)]
idmap: Vec<IdMap>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct MountParams {
location: PathBuf,
target: MountTarget,
}
pub async fn mount(
context: EffectContext,
MountParams {
location,
target:
MountTarget {
package_id,
volume_id,
subpath,
readonly,
filetype,
idmap,
},
}: MountParams,
) -> Result<(), Error> {
let context = context.deref()?;
let subpath = subpath.unwrap_or_default();
let subpath = subpath.strip_prefix("/").unwrap_or(&subpath);
let source = data_dir(DATA_DIR, &package_id, &volume_id).join(subpath);
let location = location.strip_prefix("/").unwrap_or(&location);
let mountpoint = context
.seed
.persistent_container
.lxc_container
.get()
.or_not_found("lxc container")?
.rootfs_dir()
.join(location);
if is_mountpoint(&mountpoint).await? {
unmount(&mountpoint, true).await?;
}
IdMapped::new(
Bind::new(source).with_type(filetype).recursive(true),
IdMap::stack(
vec![IdMap {
from_id: 0,
to_id: 100000,
range: 65536,
}],
idmap,
),
)
.mount(
mountpoint,
if readonly {
MountType::ReadOnly
} else {
MountType::ReadWrite
},
)
.await?;
Ok(())
}
pub async fn get_installed_packages(context: EffectContext) -> Result<BTreeSet<PackageId>, Error> {
context
.deref()?
.seed
.ctx
.db
.peek()
.await
.into_public()
.into_package_data()
.keys()
}
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase", tag = "kind")]
#[serde(rename_all_fields = "camelCase")]
#[ts(export)]
pub enum DependencyRequirement {
Running {
id: PackageId,
health_checks: BTreeSet<HealthCheckId>,
#[ts(type = "string")]
version_range: VersionRange,
},
Exists {
id: PackageId,
#[ts(type = "string")]
version_range: VersionRange,
},
}
// filebrowser:exists,bitcoind:running:foo+bar+baz
impl FromStr for DependencyRequirement {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once(':') {
Some((id, "e")) | Some((id, "exists")) => Ok(Self::Exists {
id: id.parse()?,
version_range: "*".parse()?, // TODO
}),
Some((id, rest)) => {
let health_checks = match rest.split_once(':') {
Some(("r", rest)) | Some(("running", rest)) => rest
.split('+')
.map(|id| id.parse().map_err(Error::from))
.collect(),
Some((kind, _)) => Err(Error::new(
eyre!(
"{}",
t!(
"service.effects.dependency.unknown-dependency-kind",
kind = kind
)
),
ErrorKind::InvalidRequest,
)),
None => match rest {
"r" | "running" => Ok(BTreeSet::new()),
kind => Err(Error::new(
eyre!(
"{}",
t!(
"service.effects.dependency.unknown-dependency-kind",
kind = kind
)
),
ErrorKind::InvalidRequest,
)),
},
}?;
Ok(Self::Running {
id: id.parse()?,
health_checks,
version_range: "*".parse()?, // TODO
})
}
None => Ok(Self::Running {
id: s.parse()?,
health_checks: BTreeSet::new(),
version_range: "*".parse()?, // TODO
}),
}
}
}
impl ValueParserFactory for DependencyRequirement {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {
FromStrParser::new()
}
}
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "camelCase")]
#[ts(export)]
pub struct SetDependenciesParams {
dependencies: Vec<DependencyRequirement>,
}
pub async fn set_dependencies(
context: EffectContext,
SetDependenciesParams { dependencies }: SetDependenciesParams,
) -> Result<(), Error> {
let context = context.deref()?;
let id = &context.seed.id;
let mut deps = BTreeMap::new();
for dependency in dependencies {
let (dep_id, kind, version_range) = match dependency {
DependencyRequirement::Exists { id, version_range } => {
(id, CurrentDependencyKind::Exists, version_range)
}
DependencyRequirement::Running {
id,
health_checks,
version_range,
} => (
id,
CurrentDependencyKind::Running { health_checks },
version_range,
),
};
let info = CurrentDependencyInfo {
title: context
.seed
.persistent_container
.s9pk
.dependency_metadata(&dep_id)
.await?
.map(|m| m.title),
icon: context
.seed
.persistent_container
.s9pk
.dependency_icon_data_url(&dep_id)
.await?,
kind,
version_range,
};
deps.insert(dep_id, info);
}
context
.seed
.ctx
.db
.mutate(|db| {
db.as_public_mut()
.as_package_data_mut()
.as_idx_mut(id)
.or_not_found(id)?
.as_current_dependencies_mut()
.ser(&CurrentDependencies(deps))
})
.await
.result
}
pub async fn get_dependencies(context: EffectContext) -> Result<Vec<DependencyRequirement>, Error> {
let context = context.deref()?;
let id = &context.seed.id;
let db = context.seed.ctx.db.peek().await;
let data = db
.as_public()
.as_package_data()
.as_idx(id)
.or_not_found(id)?
.as_current_dependencies()
.de()?;
Ok(data
.0
.into_iter()
.map(|(id, current_dependency_info)| {
let CurrentDependencyInfo {
version_range,
kind,
..
} = current_dependency_info;
match kind {
CurrentDependencyKind::Exists => {
DependencyRequirement::Exists { id, version_range }
}
CurrentDependencyKind::Running { health_checks } => {
DependencyRequirement::Running {
id,
health_checks,
version_range,
}
}
}
})
.collect())
}
#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CheckDependenciesParam {
#[ts(optional)]
package_ids: Option<Vec<PackageId>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CheckDependenciesResult {
package_id: PackageId,
title: Option<String>,
installed_version: Option<VersionString>,
satisfies: BTreeSet<VersionString>,
is_running: bool,
tasks: BTreeMap<ReplayId, TaskEntry>,
health_checks: BTreeMap<HealthCheckId, NamedHealthCheckResult>,
}
pub async fn check_dependencies(
context: EffectContext,
CheckDependenciesParam { package_ids }: CheckDependenciesParam,
) -> Result<Vec<CheckDependenciesResult>, Error> {
let context = context.deref()?;
let db = context.seed.ctx.db.peek().await;
let pde = db
.as_public()
.as_package_data()
.as_idx(&context.seed.id)
.or_not_found(&context.seed.id)?;
let current_dependencies = pde.as_current_dependencies().de()?;
let tasks = pde.as_tasks().de()?;
let package_dependency_info: Vec<_> = package_ids
.unwrap_or_else(|| current_dependencies.0.keys().cloned().collect())
.into_iter()
.filter_map(|x| {
let info = current_dependencies.0.get(&x)?;
Some((x, info))
})
.collect();
let mut results = Vec::with_capacity(package_dependency_info.len());
for (package_id, dependency_info) in package_dependency_info {
let title = dependency_info.title.clone();
let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else {
let tasks = tasks
.iter()
.filter(|(_, v)| v.task.package_id == package_id)
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
results.push(CheckDependenciesResult {
package_id,
title: title.map(|t| t.localized()),
installed_version: None,
satisfies: BTreeSet::new(),
is_running: false,
tasks,
health_checks: Default::default(),
});
continue;
};
let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
let installed_version = manifest.as_version().de()?.into_version();
let satisfies = manifest.as_satisfies().de()?;
let installed_version = Some(installed_version.clone().into());
let is_running = package
.as_status_info()
.as_started()
.transpose_ref()
.is_some();
let health_checks = package.as_status_info().as_health().de()?;
let tasks = tasks
.iter()
.filter(|(_, v)| v.task.package_id == package_id)
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
results.push(CheckDependenciesResult {
package_id,
title: title.map(|t| t.localized()),
installed_version,
satisfies,
is_running,
tasks,
health_checks,
});
}
Ok(results)
}
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct GetServiceManifestParams {
pub package_id: PackageId,
#[ts(optional)]
#[arg(skip)]
pub callback: Option<CallbackId>,
}
pub async fn get_service_manifest(
context: EffectContext,
GetServiceManifestParams {
package_id,
callback,
}: GetServiceManifestParams,
) -> Result<Manifest, Error> {
let context = context.deref()?;
if let Some(callback) = callback {
let callback = callback.register(&context.seed.persistent_container);
context
.seed
.ctx
.callbacks
.add_get_service_manifest(package_id.clone(), CallbackHandler::new(&context, callback));
}
let db = context.seed.ctx.db.peek().await;
let manifest = db
.as_public()
.as_package_data()
.as_idx(&package_id)
.or_not_found(&package_id)?
.as_state_info()
.as_manifest(ManifestPreference::New)
.de()?;
Ok(manifest)
}