mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 04:01:58 +00:00
Feature/callbacks (#2678)
* wip * initialize callbacks * wip * smtp * list_service_interfaces * wip * wip * fix domains * fix hostname handling in NetService * misc fixes * getInstalledPackages * misc fixes * publish v6 lib * refactor service effects * fix import * fix container runtime * fix tests * apply suggestions from review
This commit is contained in:
@@ -28,9 +28,6 @@ set +e
|
||||
fail=
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl)"; then
|
||||
fail=true
|
||||
fi
|
||||
if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl)"; then
|
||||
fail=true
|
||||
fi
|
||||
42
core/build-startbox.sh
Executable file
42
core/build-startbox.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
set -e
|
||||
shopt -s expand_aliases
|
||||
|
||||
if [ -z "$ARCH" ]; then
|
||||
ARCH=$(uname -m)
|
||||
fi
|
||||
|
||||
USE_TTY=
|
||||
if tty -s; then
|
||||
USE_TTY="-it"
|
||||
fi
|
||||
|
||||
cd ..
|
||||
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
|
||||
RUSTFLAGS=""
|
||||
|
||||
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||
|
||||
set +e
|
||||
fail=
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl)"; then
|
||||
fail=true
|
||||
fi
|
||||
set -e
|
||||
cd core
|
||||
|
||||
sudo chown -R $USER target
|
||||
sudo chown -R $USER ~/.cargo
|
||||
|
||||
if [ -n "$fail" ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -61,6 +61,11 @@ impl Borrow<str> for PackageId {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<'a> Borrow<str> for &'a PackageId {
|
||||
fn borrow(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl AsRef<Path> for PackageId {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref().as_ref()
|
||||
|
||||
@@ -4,8 +4,6 @@ use crate::{ActionId, PackageId};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ProcedureName {
|
||||
StartMain,
|
||||
StopMain,
|
||||
GetConfig,
|
||||
SetConfig,
|
||||
CreateBackup,
|
||||
@@ -25,8 +23,6 @@ impl ProcedureName {
|
||||
match self {
|
||||
ProcedureName::Init => "/init".to_string(),
|
||||
ProcedureName::Uninit => "/uninit".to_string(),
|
||||
ProcedureName::StartMain => "/main/start".to_string(),
|
||||
ProcedureName::StopMain => "/main/stop".to_string(),
|
||||
ProcedureName::SetConfig => "/config/set".to_string(),
|
||||
ProcedureName::GetConfig => "/config/get".to_string(),
|
||||
ProcedureName::CreateBackup => "/backup/create".to_string(),
|
||||
|
||||
@@ -319,6 +319,7 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) {
|
||||
pub struct ListParams {
|
||||
#[arg(skip)]
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__auth_session")] // from Auth middleware
|
||||
session: InternedString,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use imbl_value::InternedString;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use openssl::x509::X509;
|
||||
use patch_db::Value;
|
||||
@@ -97,7 +98,7 @@ impl OsBackupV0 {
|
||||
#[serde(rename = "kebab-case")]
|
||||
struct OsBackupV1 {
|
||||
server_id: String, // uuidv4
|
||||
hostname: String, // embassy-<adjective>-<noun>
|
||||
hostname: InternedString, // embassy-<adjective>-<noun>
|
||||
net_key: Base64<[u8; 32]>, // Ed25519 Secret Key
|
||||
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
|
||||
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
|
||||
@@ -127,7 +128,7 @@ impl OsBackupV1 {
|
||||
|
||||
struct OsBackupV2 {
|
||||
server_id: String, // uuidv4
|
||||
hostname: String, // <adjective>-<noun>
|
||||
hostname: InternedString, // <adjective>-<noun>
|
||||
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
|
||||
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
|
||||
ssh_key: Pem<ssh_key::PrivateKey>, // PEM Encoded OpenSSH Key
|
||||
|
||||
@@ -15,7 +15,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
|
||||
EmbassyLogger::init();
|
||||
if let Err(e) = CliApp::new(
|
||||
|cfg: ContainerClientConfig| Ok(ContainerCliContext::init(cfg)),
|
||||
crate::service::service_effect_handler::service_effect_handler(),
|
||||
crate::service::effects::handler(),
|
||||
)
|
||||
.run(args)
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@ use crate::net::wifi::WpaCli;
|
||||
use crate::prelude::*;
|
||||
use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle};
|
||||
use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations};
|
||||
use crate::service::effects::callbacks::ServiceCallbacks;
|
||||
use crate::service::ServiceMap;
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::system::get_mem_info;
|
||||
@@ -52,6 +53,7 @@ pub struct RpcContextSeed {
|
||||
pub lxc_manager: Arc<LxcManager>,
|
||||
pub open_authed_continuations: OpenAuthedContinuations<InternedString>,
|
||||
pub rpc_continuations: RpcContinuations,
|
||||
pub callbacks: ServiceCallbacks,
|
||||
pub wifi_manager: Option<Arc<RwLock<WpaCli>>>,
|
||||
pub current_secret: Arc<Jwk>,
|
||||
pub client: Client,
|
||||
@@ -225,6 +227,7 @@ impl RpcContext {
|
||||
lxc_manager: Arc::new(LxcManager::new()),
|
||||
open_authed_continuations: OpenAuthedContinuations::new(),
|
||||
rpc_continuations: RpcContinuations::new(),
|
||||
callbacks: Default::default(),
|
||||
wifi_manager: wifi_interface
|
||||
.clone()
|
||||
.map(|i| Arc::new(RwLock::new(WpaCli::init(i)))),
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::time::Duration;
|
||||
|
||||
use futures::{Future, StreamExt};
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use imbl_value::InternedString;
|
||||
use josekit::jwk::Jwk;
|
||||
use patch_db::PatchDb;
|
||||
use rpc_toolkit::Context;
|
||||
@@ -40,7 +41,8 @@ lazy_static::lazy_static! {
|
||||
#[ts(export)]
|
||||
pub struct SetupResult {
|
||||
pub tor_address: String,
|
||||
pub lan_address: String,
|
||||
#[ts(type = "string")]
|
||||
pub lan_address: InternedString,
|
||||
pub root_ca: String,
|
||||
}
|
||||
impl TryFrom<&AccountInfo> for SetupResult {
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::db::model::package::AllPackageData;
|
||||
use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr};
|
||||
use crate::prelude::*;
|
||||
use crate::progress::FullProgress;
|
||||
use crate::system::SmtpValue;
|
||||
use crate::util::cpupower::Governor;
|
||||
use crate::version::{Current, VersionT};
|
||||
use crate::{ARCH, PLATFORM};
|
||||
@@ -107,7 +108,8 @@ pub struct ServerInfo {
|
||||
#[ts(type = "string")]
|
||||
pub platform: InternedString,
|
||||
pub id: String,
|
||||
pub hostname: String,
|
||||
#[ts(type = "string")]
|
||||
pub hostname: InternedString,
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
#[ts(type = "string | null")]
|
||||
@@ -135,7 +137,7 @@ pub struct ServerInfo {
|
||||
#[serde(default)]
|
||||
pub zram: bool,
|
||||
pub governor: Option<Governor>,
|
||||
pub smtp: Option<String>,
|
||||
pub smtp: Option<SmtpValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use imbl_value::InternedString;
|
||||
use lazy_format::lazy_format;
|
||||
use rand::{thread_rng, Rng};
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
@@ -5,7 +7,7 @@ use tracing::instrument;
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ErrorKind};
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Hostname(pub String);
|
||||
pub struct Hostname(pub InternedString);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ADJECTIVES: Vec<String> = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect();
|
||||
@@ -18,15 +20,16 @@ impl AsRef<str> for Hostname {
|
||||
}
|
||||
|
||||
impl Hostname {
|
||||
pub fn lan_address(&self) -> String {
|
||||
format!("https://{}.local", self.0)
|
||||
pub fn lan_address(&self) -> InternedString {
|
||||
InternedString::from_display(&lazy_format!("https://{}.local", self.0))
|
||||
}
|
||||
|
||||
pub fn local_domain_name(&self) -> String {
|
||||
format!("{}.local", self.0)
|
||||
pub fn local_domain_name(&self) -> InternedString {
|
||||
InternedString::from_display(&lazy_format!("{}.local", self.0))
|
||||
}
|
||||
pub fn no_dot_host_name(&self) -> String {
|
||||
self.0.to_owned()
|
||||
|
||||
pub fn no_dot_host_name(&self) -> InternedString {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +37,9 @@ pub fn generate_hostname() -> Hostname {
|
||||
let mut rng = thread_rng();
|
||||
let adjective = &ADJECTIVES[rng.gen_range(0..ADJECTIVES.len())];
|
||||
let noun = &NOUNS[rng.gen_range(0..NOUNS.len())];
|
||||
Hostname(format!("{adjective}-{noun}"))
|
||||
Hostname(InternedString::from_display(&lazy_format!(
|
||||
"{adjective}-{noun}"
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn generate_id() -> String {
|
||||
@@ -48,12 +53,12 @@ pub async fn get_current_hostname() -> Result<Hostname, Error> {
|
||||
.invoke(ErrorKind::ParseSysInfo)
|
||||
.await?;
|
||||
let out_string = String::from_utf8(out)?;
|
||||
Ok(Hostname(out_string.trim().to_owned()))
|
||||
Ok(Hostname(out_string.trim().into()))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> {
|
||||
let hostname: &String = &hostname.0;
|
||||
let hostname = &*hostname.0;
|
||||
Command::new("hostnamectl")
|
||||
.arg("--static")
|
||||
.arg("set-hostname")
|
||||
|
||||
@@ -224,6 +224,18 @@ pub fn server<C: Context>() -> ParentHandler<C> {
|
||||
})
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"set-smtp",
|
||||
from_fn_async(system::set_system_smtp)
|
||||
.no_display()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"clear-smtp",
|
||||
from_fn_async(system::clear_system_smtp)
|
||||
.no_display()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn package<C: Context>() -> ParentHandler<C> {
|
||||
|
||||
@@ -34,7 +34,7 @@ struct Resolver {
|
||||
impl Resolver {
|
||||
async fn resolve(&self, name: &Name) -> Option<Vec<Ipv4Addr>> {
|
||||
match name.iter().next_back() {
|
||||
Some(b"embassy") => {
|
||||
Some(b"embassy") | Some(b"startos") => {
|
||||
if let Some(pkg) = name.iter().rev().skip(1).next() {
|
||||
if let Some(ip) = self.services.read().await.get(&Some(
|
||||
std::str::from_utf8(pkg)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use imbl_value::InternedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use torut::onion::OnionAddressV3;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "kind")]
|
||||
@@ -11,4 +17,32 @@ pub enum HostAddress {
|
||||
#[ts(type = "string")]
|
||||
address: OnionAddressV3,
|
||||
},
|
||||
Domain {
|
||||
#[ts(type = "string")]
|
||||
address: InternedString,
|
||||
},
|
||||
}
|
||||
|
||||
impl FromStr for HostAddress {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Some(addr) = s.strip_suffix(".onion") {
|
||||
Ok(HostAddress::Onion {
|
||||
address: addr
|
||||
.parse::<OnionAddressV3>()
|
||||
.with_kind(ErrorKind::ParseUrl)?,
|
||||
})
|
||||
} else {
|
||||
Ok(HostAddress::Domain { address: s.into() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HostAddress {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Onion { address } => write!(f, "{address}"),
|
||||
Self::Domain { address } => write!(f, "{address}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ impl Host {
|
||||
hostname_info: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
pub fn addresses(&self) -> impl Iterator<Item = &HostAddress> {
|
||||
// TODO: handle primary
|
||||
self.addresses.iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::sync::{Arc, Weak};
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use imbl::OrdMap;
|
||||
use imbl_value::InternedString;
|
||||
use models::{HostId, OptionExt, PackageId};
|
||||
use torut::onion::{OnionAddressV3, TorSecretKeyV3};
|
||||
use tracing::instrument;
|
||||
@@ -67,6 +68,14 @@ impl PreInitNetController {
|
||||
alpn.clone(),
|
||||
)
|
||||
.await?;
|
||||
self.vhost
|
||||
.add(
|
||||
Some("startos".into()),
|
||||
443,
|
||||
([127, 0, 0, 1], 80).into(),
|
||||
alpn.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// LAN IP
|
||||
self.os_bindings.push(
|
||||
@@ -113,7 +122,9 @@ impl PreInitNetController {
|
||||
self.os_bindings.push(
|
||||
self.vhost
|
||||
.add(
|
||||
Some(tor_key.public().get_onion_address().to_string()),
|
||||
Some(InternedString::from_display(
|
||||
&tor_key.public().get_onion_address(),
|
||||
)),
|
||||
443,
|
||||
([127, 0, 0, 1], 80).into(),
|
||||
alpn.clone(),
|
||||
@@ -189,7 +200,15 @@ impl NetController {
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct HostBinds {
|
||||
lan: BTreeMap<u16, (LanInfo, Option<AddSslOptions>, Vec<Arc<()>>)>,
|
||||
lan: BTreeMap<
|
||||
u16,
|
||||
(
|
||||
LanInfo,
|
||||
Option<AddSslOptions>,
|
||||
BTreeSet<InternedString>,
|
||||
Vec<Arc<()>>,
|
||||
),
|
||||
>,
|
||||
tor: BTreeMap<OnionAddressV3, (OrdMap<u16, SocketAddr>, Vec<Arc<()>>)>,
|
||||
}
|
||||
|
||||
@@ -234,20 +253,35 @@ impl NetService {
|
||||
.await?;
|
||||
self.update(id, host).await
|
||||
}
|
||||
|
||||
pub async fn clear_bindings(&mut self) -> Result<(), Error> {
|
||||
// TODO BLUJ
|
||||
Ok(())
|
||||
let ctrl = self.net_controller()?;
|
||||
let mut errors = ErrorCollection::new();
|
||||
for (_, binds) in std::mem::take(&mut self.binds) {
|
||||
for (_, (lan, _, _, rc)) in binds.lan {
|
||||
drop(rc);
|
||||
if let Some(external) = lan.assigned_ssl_port {
|
||||
ctrl.vhost.gc(None, external).await?;
|
||||
}
|
||||
if let Some(external) = lan.assigned_port {
|
||||
ctrl.forward.gc(external).await?;
|
||||
}
|
||||
}
|
||||
for (addr, (_, rcs)) in binds.tor {
|
||||
drop(rcs);
|
||||
errors.handle(ctrl.tor.gc(Some(addr), None).await);
|
||||
}
|
||||
}
|
||||
std::mem::take(&mut self.dns);
|
||||
errors.handle(ctrl.dns.gc(Some(self.id.clone()), self.ip).await);
|
||||
errors.into_result()
|
||||
}
|
||||
|
||||
async fn update(&mut self, id: HostId, host: Host) -> Result<(), Error> {
|
||||
let ctrl = self.net_controller()?;
|
||||
let mut hostname_info = BTreeMap::new();
|
||||
let binds = {
|
||||
if !self.binds.contains_key(&id) {
|
||||
self.binds.insert(id.clone(), Default::default());
|
||||
}
|
||||
self.binds.get_mut(&id).unwrap()
|
||||
};
|
||||
let binds = self.binds.entry(id.clone()).or_default();
|
||||
|
||||
let peek = ctrl.db.peek().await;
|
||||
|
||||
// LAN
|
||||
@@ -256,37 +290,71 @@ impl NetService {
|
||||
let hostname = server_info.as_hostname().de()?;
|
||||
for (port, bind) in &host.bindings {
|
||||
let old_lan_bind = binds.lan.remove(port);
|
||||
let old_lan_port = old_lan_bind.as_ref().map(|(external, _, _)| *external);
|
||||
let lan_bind = old_lan_bind
|
||||
.filter(|(external, ssl, _)| ssl == &bind.options.add_ssl && bind.lan == *external); // only keep existing binding if relevant details match
|
||||
.as_ref()
|
||||
.filter(|(external, ssl, _, _)| {
|
||||
ssl == &bind.options.add_ssl && bind.lan == *external
|
||||
})
|
||||
.cloned(); // only keep existing binding if relevant details match
|
||||
if bind.lan.assigned_port.is_some() || bind.lan.assigned_ssl_port.is_some() {
|
||||
let new_lan_bind = if let Some(b) = lan_bind {
|
||||
b
|
||||
} else {
|
||||
let mut rcs = Vec::with_capacity(2);
|
||||
let mut rcs = Vec::with_capacity(2 + host.addresses.len());
|
||||
let mut hostnames = BTreeSet::new();
|
||||
if let Some(ssl) = &bind.options.add_ssl {
|
||||
let external = bind
|
||||
.lan
|
||||
.assigned_ssl_port
|
||||
.or_not_found("assigned ssl port")?;
|
||||
let target = (self.ip, *port).into();
|
||||
let connect_ssl = if let Some(alpn) = ssl.alpn.clone() {
|
||||
Err(alpn)
|
||||
} else {
|
||||
if bind.options.secure.as_ref().map_or(false, |s| s.ssl) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AlpnInfo::Reflect)
|
||||
}
|
||||
};
|
||||
rcs.push(
|
||||
ctrl.vhost
|
||||
.add(
|
||||
None,
|
||||
external,
|
||||
(self.ip, *port).into(),
|
||||
if let Some(alpn) = ssl.alpn.clone() {
|
||||
Err(alpn)
|
||||
} else {
|
||||
if bind.options.secure.as_ref().map_or(false, |s| s.ssl) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AlpnInfo::Reflect)
|
||||
}
|
||||
},
|
||||
)
|
||||
.add(None, external, target, connect_ssl.clone())
|
||||
.await?,
|
||||
);
|
||||
for address in host.addresses() {
|
||||
match address {
|
||||
HostAddress::Onion { address } => {
|
||||
let hostname = InternedString::from_display(address);
|
||||
if hostnames.insert(hostname.clone()) {
|
||||
rcs.push(
|
||||
ctrl.vhost
|
||||
.add(
|
||||
Some(hostname),
|
||||
external,
|
||||
target,
|
||||
connect_ssl.clone(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
}
|
||||
HostAddress::Domain { address } => {
|
||||
if hostnames.insert(address.clone()) {
|
||||
rcs.push(
|
||||
ctrl.vhost
|
||||
.add(
|
||||
Some(address.clone()),
|
||||
external,
|
||||
target,
|
||||
connect_ssl.clone(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(security) = bind.options.secure {
|
||||
if bind.options.add_ssl.is_some() && security.ssl {
|
||||
@@ -297,7 +365,7 @@ impl NetService {
|
||||
rcs.push(ctrl.forward.add(external, (self.ip, *port).into()).await?);
|
||||
}
|
||||
}
|
||||
(bind.lan, bind.options.add_ssl.clone(), rcs)
|
||||
(bind.lan, bind.options.add_ssl.clone(), hostnames, rcs)
|
||||
};
|
||||
let mut bind_hostname_info: Vec<HostnameInfo> =
|
||||
hostname_info.remove(port).unwrap_or_default();
|
||||
@@ -337,9 +405,12 @@ impl NetService {
|
||||
hostname_info.insert(*port, bind_hostname_info);
|
||||
binds.lan.insert(*port, new_lan_bind);
|
||||
}
|
||||
if let Some(lan) = old_lan_port {
|
||||
if let Some((lan, _, hostnames, _)) = old_lan_bind {
|
||||
if let Some(external) = lan.assigned_ssl_port {
|
||||
ctrl.vhost.gc(None, external).await?;
|
||||
for hostname in hostnames {
|
||||
ctrl.vhost.gc(Some(hostname), external).await?;
|
||||
}
|
||||
}
|
||||
if let Some(external) = lan.assigned_port {
|
||||
ctrl.forward.gc(external).await?;
|
||||
@@ -347,18 +418,21 @@ impl NetService {
|
||||
}
|
||||
}
|
||||
let mut removed = BTreeSet::new();
|
||||
binds.lan.retain(|internal, (external, _, _)| {
|
||||
binds.lan.retain(|internal, (external, _, hostnames, _)| {
|
||||
if host.bindings.contains_key(internal) {
|
||||
true
|
||||
} else {
|
||||
removed.insert(*external);
|
||||
removed.insert((*external, std::mem::take(hostnames)));
|
||||
|
||||
false
|
||||
}
|
||||
});
|
||||
for lan in removed {
|
||||
for (lan, hostnames) in removed {
|
||||
if let Some(external) = lan.assigned_ssl_port {
|
||||
ctrl.vhost.gc(None, external).await?;
|
||||
for hostname in hostnames {
|
||||
ctrl.vhost.gc(Some(hostname), external).await?;
|
||||
}
|
||||
}
|
||||
if let Some(external) = lan.assigned_port {
|
||||
ctrl.forward.gc(external).await?;
|
||||
@@ -401,48 +475,44 @@ impl NetService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut keep_tor_addrs = BTreeSet::new();
|
||||
for addr in match host.kind {
|
||||
HostKind::Multi => {
|
||||
// itertools::Either::Left(
|
||||
host.addresses.iter()
|
||||
// )
|
||||
} // HostKind::Single | HostKind::Static => itertools::Either::Right(&host.primary),
|
||||
} {
|
||||
match addr {
|
||||
HostAddress::Onion { address } => {
|
||||
keep_tor_addrs.insert(address);
|
||||
let old_tor_bind = binds.tor.remove(address);
|
||||
let tor_bind = old_tor_bind.filter(|(ports, _)| ports == &tor_binds);
|
||||
let new_tor_bind = if let Some(tor_bind) = tor_bind {
|
||||
tor_bind
|
||||
} else {
|
||||
let key = peek
|
||||
.as_private()
|
||||
.as_key_store()
|
||||
.as_onion()
|
||||
.get_key(address)?;
|
||||
let rcs = ctrl
|
||||
.tor
|
||||
.add(key, tor_binds.clone().into_iter().collect())
|
||||
.await?;
|
||||
(tor_binds.clone(), rcs)
|
||||
};
|
||||
for (internal, ports) in &tor_hostname_ports {
|
||||
let mut bind_hostname_info =
|
||||
hostname_info.remove(internal).unwrap_or_default();
|
||||
bind_hostname_info.push(HostnameInfo::Onion {
|
||||
hostname: OnionHostname {
|
||||
value: address.to_string(),
|
||||
port: ports.non_ssl,
|
||||
ssl_port: ports.ssl,
|
||||
},
|
||||
});
|
||||
hostname_info.insert(*internal, bind_hostname_info);
|
||||
}
|
||||
binds.tor.insert(address.clone(), new_tor_bind);
|
||||
}
|
||||
for tor_addr in host.addresses().filter_map(|a| {
|
||||
if let HostAddress::Onion { address } = a {
|
||||
Some(address)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
keep_tor_addrs.insert(tor_addr);
|
||||
let old_tor_bind = binds.tor.remove(tor_addr);
|
||||
let tor_bind = old_tor_bind.filter(|(ports, _)| ports == &tor_binds);
|
||||
let new_tor_bind = if let Some(tor_bind) = tor_bind {
|
||||
tor_bind
|
||||
} else {
|
||||
let key = peek
|
||||
.as_private()
|
||||
.as_key_store()
|
||||
.as_onion()
|
||||
.get_key(tor_addr)?;
|
||||
let rcs = ctrl
|
||||
.tor
|
||||
.add(key, tor_binds.clone().into_iter().collect())
|
||||
.await?;
|
||||
(tor_binds.clone(), rcs)
|
||||
};
|
||||
for (internal, ports) in &tor_hostname_ports {
|
||||
let mut bind_hostname_info = hostname_info.remove(internal).unwrap_or_default();
|
||||
bind_hostname_info.push(HostnameInfo::Onion {
|
||||
hostname: OnionHostname {
|
||||
value: tor_addr.to_string(),
|
||||
port: ports.non_ssl,
|
||||
ssl_port: ports.ssl,
|
||||
},
|
||||
});
|
||||
hostname_info.insert(*internal, bind_hostname_info);
|
||||
}
|
||||
binds.tor.insert(tor_addr.clone(), new_tor_bind);
|
||||
}
|
||||
for addr in binds.tor.keys() {
|
||||
if !keep_tor_addrs.contains(addr) {
|
||||
@@ -462,26 +532,8 @@ impl NetService {
|
||||
|
||||
pub async fn remove_all(mut self) -> Result<(), Error> {
|
||||
self.shutdown = true;
|
||||
let mut errors = ErrorCollection::new();
|
||||
if let Some(ctrl) = Weak::upgrade(&self.controller) {
|
||||
for (_, binds) in std::mem::take(&mut self.binds) {
|
||||
for (_, (lan, _, rc)) in binds.lan {
|
||||
drop(rc);
|
||||
if let Some(external) = lan.assigned_ssl_port {
|
||||
ctrl.vhost.gc(None, external).await?;
|
||||
}
|
||||
if let Some(external) = lan.assigned_port {
|
||||
ctrl.forward.gc(external).await?;
|
||||
}
|
||||
}
|
||||
for (addr, (_, rcs)) in binds.tor {
|
||||
drop(rcs);
|
||||
errors.handle(ctrl.tor.gc(Some(addr), None).await);
|
||||
}
|
||||
}
|
||||
std::mem::take(&mut self.dns);
|
||||
errors.handle(ctrl.dns.gc(Some(self.id.clone()), self.ip).await);
|
||||
errors.into_result()
|
||||
self.clear_bindings().await
|
||||
} else {
|
||||
tracing::warn!("NetService dropped after NetController is shutdown");
|
||||
Err(Error::new(
|
||||
@@ -495,11 +547,11 @@ impl NetService {
|
||||
self.ip
|
||||
}
|
||||
|
||||
pub fn get_ext_port(&self, host_id: HostId, internal_port: u16) -> Result<LanInfo, Error> {
|
||||
pub fn get_lan_port(&self, host_id: HostId, internal_port: u16) -> Result<LanInfo, Error> {
|
||||
let host_id_binds = self.binds.get_key_value(&host_id);
|
||||
match host_id_binds {
|
||||
Some((_, binds)) => {
|
||||
if let Some((lan, _, _)) = binds.lan.get(&internal_port) {
|
||||
if let Some((lan, _, _, _)) = binds.lan.get(&internal_port) {
|
||||
Ok(*lan)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::cmp::{min, Ordering};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::net::IpAddr;
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use futures::FutureExt;
|
||||
use imbl_value::InternedString;
|
||||
use libc::time_t;
|
||||
use openssl::asn1::{Asn1Integer, Asn1Time};
|
||||
use openssl::asn1::{Asn1Integer, Asn1Time, Asn1TimeRef};
|
||||
use openssl::bn::{BigNum, MsbOption};
|
||||
use openssl::ec::{EcGroup, EcKey};
|
||||
use openssl::hash::MessageDigest;
|
||||
@@ -17,6 +17,7 @@ use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509};
|
||||
use openssl::*;
|
||||
use patch_db::HasModel;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::account::AccountInfo;
|
||||
@@ -126,12 +127,18 @@ impl Model<CertStore> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct CertData {
|
||||
pub keys: PKeyPair,
|
||||
pub certs: CertPair,
|
||||
}
|
||||
impl CertData {
|
||||
pub fn expiration(&self) -> Result<SystemTime, Error> {
|
||||
self.certs.expiration()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FullchainCertData {
|
||||
pub root: X509,
|
||||
pub int: X509,
|
||||
@@ -144,6 +151,16 @@ impl FullchainCertData {
|
||||
pub fn fullchain_nistp256(&self) -> Vec<&X509> {
|
||||
vec![&self.leaf.certs.nistp256, &self.int, &self.root]
|
||||
}
|
||||
pub fn expiration(&self) -> Result<SystemTime, Error> {
|
||||
[
|
||||
asn1_time_to_system_time(self.root.not_after())?,
|
||||
asn1_time_to_system_time(self.int.not_after())?,
|
||||
self.leaf.expiration()?,
|
||||
]
|
||||
.into_iter()
|
||||
.min()
|
||||
.ok_or_else(|| Error::new(eyre!("unreachable"), ErrorKind::Unknown))
|
||||
}
|
||||
}
|
||||
|
||||
static CERTIFICATE_VERSION: i32 = 2; // X509 version 3 is actually encoded as '2' in the cert because fuck you.
|
||||
@@ -155,6 +172,26 @@ fn unix_time(time: SystemTime) -> time_t {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ASN1_UNIX_EPOCH: Asn1Time = Asn1Time::from_unix(0).unwrap();
|
||||
}
|
||||
|
||||
fn asn1_time_to_system_time(time: &Asn1TimeRef) -> Result<SystemTime, Error> {
|
||||
let diff = time.diff(&**ASN1_UNIX_EPOCH)?;
|
||||
let mut res = UNIX_EPOCH;
|
||||
if diff.days >= 0 {
|
||||
res += Duration::from_secs(diff.days as u64 * 86400);
|
||||
} else {
|
||||
res -= Duration::from_secs((-1 * diff.days) as u64 * 86400);
|
||||
}
|
||||
if diff.secs >= 0 {
|
||||
res += Duration::from_secs(diff.secs as u64);
|
||||
} else {
|
||||
res -= Duration::from_secs((-1 * diff.secs) as u64);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PKeyPair {
|
||||
#[serde(with = "crate::util::serde::pem")]
|
||||
@@ -162,6 +199,12 @@ pub struct PKeyPair {
|
||||
#[serde(with = "crate::util::serde::pem")]
|
||||
pub nistp256: PKey<Private>,
|
||||
}
|
||||
impl PartialEq for PKeyPair {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.ed25519.public_eq(&other.ed25519) && self.nistp256.public_eq(&other.nistp256)
|
||||
}
|
||||
}
|
||||
impl Eq for PKeyPair {}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
|
||||
pub struct CertPair {
|
||||
@@ -170,6 +213,14 @@ pub struct CertPair {
|
||||
#[serde(with = "crate::util::serde::pem")]
|
||||
pub nistp256: X509,
|
||||
}
|
||||
impl CertPair {
|
||||
pub fn expiration(&self) -> Result<SystemTime, Error> {
|
||||
Ok(min(
|
||||
asn1_time_to_system_time(self.ed25519.not_after())?,
|
||||
asn1_time_to_system_time(self.nistp256.not_after())?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn root_ca_start_time() -> Result<SystemTime, Error> {
|
||||
Ok(if check_time_is_synchronized().await? {
|
||||
|
||||
@@ -46,7 +46,7 @@ impl VHostController {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn add(
|
||||
&self,
|
||||
hostname: Option<String>,
|
||||
hostname: Option<InternedString>,
|
||||
external: u16,
|
||||
target: SocketAddr,
|
||||
connect_ssl: Result<(), AlpnInfo>, // Ok: yes, connect using ssl, pass through alpn; Err: connect tcp, use provided strategy for alpn
|
||||
@@ -70,7 +70,7 @@ impl VHostController {
|
||||
Ok(rc?)
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
pub async fn gc(&self, hostname: Option<String>, external: u16) -> Result<(), Error> {
|
||||
pub async fn gc(&self, hostname: Option<InternedString>, external: u16) -> Result<(), Error> {
|
||||
let mut writable = self.servers.lock().await;
|
||||
if let Some(server) = writable.remove(&external) {
|
||||
server.gc(hostname).await?;
|
||||
@@ -102,7 +102,7 @@ impl Default for AlpnInfo {
|
||||
}
|
||||
|
||||
struct VHostServer {
|
||||
mapping: Weak<RwLock<BTreeMap<Option<String>, BTreeMap<TargetInfo, Weak<()>>>>>,
|
||||
mapping: Weak<RwLock<BTreeMap<Option<InternedString>, BTreeMap<TargetInfo, Weak<()>>>>>,
|
||||
_thread: NonDetachingJoinHandle<()>,
|
||||
}
|
||||
impl VHostServer {
|
||||
@@ -179,7 +179,7 @@ impl VHostServer {
|
||||
}
|
||||
};
|
||||
let target_name =
|
||||
mid.client_hello().server_name().map(|s| s.to_owned());
|
||||
mid.client_hello().server_name().map(|s| s.into());
|
||||
let target = {
|
||||
let mapping = mapping.read().await;
|
||||
mapping
|
||||
@@ -208,9 +208,7 @@ impl VHostServer {
|
||||
let mut tcp_stream =
|
||||
TcpStream::connect(target.addr).await?;
|
||||
let hostnames = target_name
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.map(InternedString::intern)
|
||||
.chain(
|
||||
db.peek()
|
||||
.await
|
||||
@@ -405,7 +403,11 @@ impl VHostServer {
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
async fn add(&self, hostname: Option<String>, target: TargetInfo) -> Result<Arc<()>, Error> {
|
||||
async fn add(
|
||||
&self,
|
||||
hostname: Option<InternedString>,
|
||||
target: TargetInfo,
|
||||
) -> Result<Arc<()>, Error> {
|
||||
if let Some(mapping) = Weak::upgrade(&self.mapping) {
|
||||
let mut writable = mapping.write().await;
|
||||
let mut targets = writable.remove(&hostname).unwrap_or_default();
|
||||
@@ -424,7 +426,7 @@ impl VHostServer {
|
||||
))
|
||||
}
|
||||
}
|
||||
async fn gc(&self, hostname: Option<String>) -> Result<(), Error> {
|
||||
async fn gc(&self, hostname: Option<InternedString>) -> Result<(), Error> {
|
||||
if let Some(mapping) = Weak::upgrade(&self.mapping) {
|
||||
let mut writable = mapping.write().await;
|
||||
let mut targets = writable.remove(&hostname).unwrap_or_default();
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::path::PathBuf;
|
||||
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use exver::Version;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
|
||||
@@ -27,7 +28,6 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::serde::Base64;
|
||||
use crate::util::VersionString;
|
||||
|
||||
pub fn add_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
@@ -55,7 +55,8 @@ pub fn add_api<C: Context>() -> ParentHandler<C> {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AddAssetParams {
|
||||
pub version: VersionString,
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
#[ts(type = "string")]
|
||||
pub platform: InternedString,
|
||||
#[ts(type = "string")]
|
||||
@@ -154,7 +155,7 @@ pub struct CliAddAssetParams {
|
||||
#[arg(short = 'p', long = "platform")]
|
||||
pub platform: InternedString,
|
||||
#[arg(short = 'v', long = "version")]
|
||||
pub version: VersionString,
|
||||
pub version: Version,
|
||||
pub file: PathBuf,
|
||||
pub url: Url,
|
||||
}
|
||||
@@ -209,11 +210,18 @@ pub async fn cli_add_asset(
|
||||
hash: Base64(*blake3.as_bytes()),
|
||||
size,
|
||||
};
|
||||
let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?;
|
||||
let signature = AnySignature::Ed25519(Ed25519.sign_commitment(
|
||||
ctx.developer_key()?,
|
||||
&commitment,
|
||||
SIG_CONTEXT,
|
||||
)?);
|
||||
sign_phase.complete();
|
||||
|
||||
verify_phase.start();
|
||||
let src = HttpSource::new(ctx.client.clone(), url.clone()).await?;
|
||||
if let Some(size) = src.size().await {
|
||||
verify_phase.set_total(size);
|
||||
}
|
||||
let mut writer = verify_phase.writer(VerifyingWriter::new(
|
||||
tokio::io::sink(),
|
||||
Some((blake3::Hash::from_bytes(*commitment.hash), commitment.size)),
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::panic::UnwindSafe;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::Parser;
|
||||
use exver::Version;
|
||||
use helpers::AtomicFile;
|
||||
use imbl_value::{json, InternedString};
|
||||
use itertools::Itertools;
|
||||
@@ -21,7 +22,6 @@ use crate::registry::signer::commitment::blake3::Blake3Commitment;
|
||||
use crate::registry::signer::commitment::Commitment;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::VersionString;
|
||||
|
||||
pub fn get_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
@@ -37,7 +37,8 @@ pub fn get_api<C: Context>() -> ParentHandler<C> {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetOsAssetParams {
|
||||
pub version: VersionString,
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
#[ts(type = "string")]
|
||||
pub platform: InternedString,
|
||||
}
|
||||
@@ -91,7 +92,7 @@ pub async fn get_squashfs(
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CliGetOsAssetParams {
|
||||
pub version: VersionString,
|
||||
pub version: Version,
|
||||
pub platform: InternedString,
|
||||
#[arg(long = "download", short = 'd')]
|
||||
pub download: Option<PathBuf>,
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::panic::UnwindSafe;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use exver::Version;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
|
||||
@@ -23,7 +24,6 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::serde::Base64;
|
||||
use crate::util::VersionString;
|
||||
|
||||
pub fn sign_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
@@ -51,7 +51,8 @@ pub fn sign_api<C: Context>() -> ParentHandler<C> {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SignAssetParams {
|
||||
version: VersionString,
|
||||
#[ts(type = "string")]
|
||||
version: Version,
|
||||
#[ts(type = "string")]
|
||||
platform: InternedString,
|
||||
#[ts(skip)]
|
||||
@@ -137,7 +138,7 @@ pub struct CliSignAssetParams {
|
||||
#[arg(short = 'p', long = "platform")]
|
||||
pub platform: InternedString,
|
||||
#[arg(short = 'v', long = "version")]
|
||||
pub version: VersionString,
|
||||
pub version: Version,
|
||||
pub file: PathBuf,
|
||||
}
|
||||
|
||||
@@ -189,7 +190,11 @@ pub async fn cli_sign_asset(
|
||||
hash: Base64(*blake3.as_bytes()),
|
||||
size,
|
||||
};
|
||||
let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?;
|
||||
let signature = AnySignature::Ed25519(Ed25519.sign_commitment(
|
||||
ctx.developer_key()?,
|
||||
&commitment,
|
||||
SIG_CONTEXT,
|
||||
)?);
|
||||
sign_phase.complete();
|
||||
|
||||
index_phase.start();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use exver::VersionRange;
|
||||
use exver::{Version, VersionRange};
|
||||
use imbl_value::InternedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
@@ -10,14 +10,28 @@ use crate::registry::asset::RegistryAsset;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::signer::commitment::blake3::Blake3Commitment;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::util::VersionString;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct OsIndex {
|
||||
pub versions: BTreeMap<VersionString, OsVersionInfo>,
|
||||
pub versions: OsVersionInfoMap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, TS)]
|
||||
pub struct OsVersionInfoMap(
|
||||
#[ts(as = "BTreeMap::<String, OsVersionInfo>")] pub BTreeMap<Version, OsVersionInfo>,
|
||||
);
|
||||
impl Map for OsVersionInfoMap {
|
||||
type Key = Version;
|
||||
type Value = OsVersionInfo;
|
||||
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
||||
Ok(InternedString::from_display(key))
|
||||
}
|
||||
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
|
||||
Ok(InternedString::from_display(key))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use exver::VersionRange;
|
||||
use exver::{Version, VersionRange};
|
||||
use itertools::Itertools;
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -15,7 +15,6 @@ use crate::registry::context::RegistryContext;
|
||||
use crate::registry::os::index::OsVersionInfo;
|
||||
use crate::registry::signer::sign::AnyVerifyingKey;
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
|
||||
use crate::util::VersionString;
|
||||
|
||||
pub mod signer;
|
||||
|
||||
@@ -53,7 +52,8 @@ pub fn version_api<C: Context>() -> ParentHandler<C> {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AddVersionParams {
|
||||
pub version: VersionString,
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
pub headline: String,
|
||||
pub release_notes: String,
|
||||
#[ts(type = "string")]
|
||||
@@ -99,7 +99,8 @@ pub async fn add_version(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RemoveVersionParams {
|
||||
pub version: VersionString,
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
pub async fn remove_version(
|
||||
@@ -124,7 +125,7 @@ pub async fn remove_version(
|
||||
pub struct GetOsVersionParams {
|
||||
#[ts(type = "string | null")]
|
||||
#[arg(long = "src")]
|
||||
pub source: Option<VersionString>,
|
||||
pub source: Option<Version>,
|
||||
#[ts(type = "string | null")]
|
||||
#[arg(long = "target")]
|
||||
pub target: Option<VersionRange>,
|
||||
@@ -144,7 +145,7 @@ pub async fn get_version(
|
||||
server_id,
|
||||
arch,
|
||||
}: GetOsVersionParams,
|
||||
) -> Result<BTreeMap<VersionString, OsVersionInfo>, Error> {
|
||||
) -> Result<BTreeMap<Version, OsVersionInfo>, Error> {
|
||||
if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) {
|
||||
let created_at = Utc::now();
|
||||
|
||||
@@ -176,10 +177,7 @@ pub async fn get_version(
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn display_version_info<T>(
|
||||
params: WithIoFormat<T>,
|
||||
info: BTreeMap<VersionString, OsVersionInfo>,
|
||||
) {
|
||||
pub fn display_version_info<T>(params: WithIoFormat<T>, info: BTreeMap<Version, OsVersionInfo>) {
|
||||
use prettytable::*;
|
||||
|
||||
if let Some(format) = params.format {
|
||||
@@ -197,7 +195,7 @@ pub fn display_version_info<T>(
|
||||
]);
|
||||
for (version, info) in &info {
|
||||
table.add_row(row![
|
||||
version.as_str(),
|
||||
&version.to_string(),
|
||||
&info.headline,
|
||||
&info.release_notes,
|
||||
&info.iso.keys().into_iter().join(", "),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use clap::Parser;
|
||||
use exver::Version;
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
@@ -12,7 +13,6 @@ use crate::registry::context::RegistryContext;
|
||||
use crate::registry::signer::SignerInfo;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::util::serde::HandlerExtSerde;
|
||||
use crate::util::VersionString;
|
||||
|
||||
pub fn signer_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
@@ -44,7 +44,8 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct VersionSignerParams {
|
||||
pub version: VersionString,
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
pub signer: Guid,
|
||||
}
|
||||
|
||||
@@ -104,7 +105,8 @@ pub async fn remove_version_signer(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct ListVersionSignersParams {
|
||||
pub version: VersionString,
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
pub async fn list_version_signers(
|
||||
|
||||
@@ -9,7 +9,7 @@ use rpc_toolkit::{call_remote_socket, yajrc, CallRemote, Context, Empty};
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::lxc::HOST_RPC_SERVER_SOCKET;
|
||||
use crate::service::service_effect_handler::EffectContext;
|
||||
use crate::service::effects::context::EffectContext;
|
||||
|
||||
#[derive(Debug, Default, Parser)]
|
||||
pub struct ContainerClientConfig {
|
||||
|
||||
101
core/startos/src/service/effects/action.rs
Normal file
101
core/startos/src/service/effects/action.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use models::{ActionId, PackageId};
|
||||
|
||||
use crate::action::ActionResult;
|
||||
use crate::db::model::package::ActionMetadata;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::effects::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExportActionParams {
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
id: ActionId,
|
||||
metadata: ActionMetadata,
|
||||
}
|
||||
pub async fn export_action(context: EffectContext, data: ExportActionParams) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = context.seed.id.clone();
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let model = db
|
||||
.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&package_id)
|
||||
.or_not_found(&package_id)?
|
||||
.as_actions_mut();
|
||||
let mut value = model.de()?;
|
||||
value
|
||||
.insert(data.id, data.metadata)
|
||||
.map(|_| ())
|
||||
.unwrap_or_default();
|
||||
model.ser(&value)
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn clear_actions(context: EffectContext) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = context.seed.id.clone();
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&package_id)
|
||||
.or_not_found(&package_id)?
|
||||
.as_actions_mut()
|
||||
.ser(&BTreeMap::new())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct ExecuteAction {
|
||||
#[serde(default)]
|
||||
#[ts(skip)]
|
||||
procedure_id: Guid,
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
action_id: ActionId,
|
||||
#[ts(type = "any")]
|
||||
input: Value,
|
||||
}
|
||||
pub async fn execute_action(
|
||||
context: EffectContext,
|
||||
ExecuteAction {
|
||||
procedure_id,
|
||||
package_id,
|
||||
action_id,
|
||||
input,
|
||||
}: ExecuteAction,
|
||||
) -> Result<ActionResult, Error> {
|
||||
let context = context.deref()?;
|
||||
|
||||
if let Some(package_id) = package_id {
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.services
|
||||
.get(&package_id)
|
||||
.await
|
||||
.as_ref()
|
||||
.or_not_found(&package_id)?
|
||||
.action(procedure_id, action_id, input)
|
||||
.await
|
||||
} else {
|
||||
context.action(procedure_id, action_id, input).await
|
||||
}
|
||||
}
|
||||
311
core/startos/src/service/effects/callbacks.rs
Normal file
311
core/startos/src/service/effects/callbacks.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
use std::cmp::min;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use futures::future::join_all;
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use imbl::{vector, Vector};
|
||||
use imbl_value::InternedString;
|
||||
use models::{HostId, PackageId, ServiceInterfaceId};
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::net::ssl::FullchainCertData;
|
||||
use crate::prelude::*;
|
||||
use crate::service::effects::context::EffectContext;
|
||||
use crate::service::effects::net::ssl::Algorithm;
|
||||
use crate::service::rpc::CallbackHandle;
|
||||
use crate::service::{Service, ServiceActorSeed};
|
||||
use crate::util::collections::EqMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ServiceCallbacks(Mutex<ServiceCallbackMap>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct ServiceCallbackMap {
|
||||
get_service_interface: BTreeMap<(PackageId, ServiceInterfaceId), Vec<CallbackHandler>>,
|
||||
list_service_interfaces: BTreeMap<PackageId, Vec<CallbackHandler>>,
|
||||
get_system_smtp: Vec<CallbackHandler>,
|
||||
get_host_info: BTreeMap<(PackageId, HostId), Vec<CallbackHandler>>,
|
||||
get_ssl_certificate: EqMap<
|
||||
(BTreeSet<InternedString>, FullchainCertData, Algorithm),
|
||||
(NonDetachingJoinHandle<()>, Vec<CallbackHandler>),
|
||||
>,
|
||||
get_store: BTreeMap<PackageId, BTreeMap<JsonPointer, Vec<CallbackHandler>>>,
|
||||
}
|
||||
|
||||
impl ServiceCallbacks {
|
||||
fn mutate<T>(&self, f: impl FnOnce(&mut ServiceCallbackMap) -> T) -> T {
|
||||
let mut this = self.0.lock().unwrap();
|
||||
f(&mut *this)
|
||||
}
|
||||
|
||||
pub fn gc(&self) {
|
||||
self.mutate(|this| {
|
||||
this.get_service_interface.retain(|_, v| {
|
||||
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
|
||||
!v.is_empty()
|
||||
});
|
||||
this.list_service_interfaces.retain(|_, v| {
|
||||
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
|
||||
!v.is_empty()
|
||||
});
|
||||
this.get_system_smtp
|
||||
.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
|
||||
this.get_host_info.retain(|_, v| {
|
||||
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
|
||||
!v.is_empty()
|
||||
});
|
||||
this.get_ssl_certificate.retain(|_, (_, v)| {
|
||||
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
|
||||
!v.is_empty()
|
||||
});
|
||||
this.get_store.retain(|_, v| {
|
||||
v.retain(|_, v| {
|
||||
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
|
||||
!v.is_empty()
|
||||
});
|
||||
!v.is_empty()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn add_get_service_interface(
|
||||
&self,
|
||||
package_id: PackageId,
|
||||
service_interface_id: ServiceInterfaceId,
|
||||
handler: CallbackHandler,
|
||||
) {
|
||||
self.mutate(|this| {
|
||||
this.get_service_interface
|
||||
.entry((package_id, service_interface_id))
|
||||
.or_default()
|
||||
.push(handler);
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_service_interface(
|
||||
&self,
|
||||
id: &(PackageId, ServiceInterfaceId),
|
||||
) -> Option<CallbackHandlers> {
|
||||
self.mutate(|this| {
|
||||
Some(CallbackHandlers(
|
||||
this.get_service_interface.remove(id).unwrap_or_default(),
|
||||
))
|
||||
.filter(|cb| !cb.0.is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn add_list_service_interfaces(
|
||||
&self,
|
||||
package_id: PackageId,
|
||||
handler: CallbackHandler,
|
||||
) {
|
||||
self.mutate(|this| {
|
||||
this.list_service_interfaces
|
||||
.entry(package_id)
|
||||
.or_default()
|
||||
.push(handler);
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn list_service_interfaces(&self, id: &PackageId) -> Option<CallbackHandlers> {
|
||||
self.mutate(|this| {
|
||||
Some(CallbackHandlers(
|
||||
this.list_service_interfaces.remove(id).unwrap_or_default(),
|
||||
))
|
||||
.filter(|cb| !cb.0.is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn add_get_system_smtp(&self, handler: CallbackHandler) {
|
||||
self.mutate(|this| {
|
||||
this.get_system_smtp.push(handler);
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_system_smtp(&self) -> Option<CallbackHandlers> {
|
||||
self.mutate(|this| {
|
||||
Some(CallbackHandlers(std::mem::take(&mut this.get_system_smtp)))
|
||||
.filter(|cb| !cb.0.is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn add_get_host_info(
|
||||
&self,
|
||||
package_id: PackageId,
|
||||
host_id: HostId,
|
||||
handler: CallbackHandler,
|
||||
) {
|
||||
self.mutate(|this| {
|
||||
this.get_host_info
|
||||
.entry((package_id, host_id))
|
||||
.or_default()
|
||||
.push(handler);
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_host_info(&self, id: &(PackageId, HostId)) -> Option<CallbackHandlers> {
|
||||
self.mutate(|this| {
|
||||
Some(CallbackHandlers(
|
||||
this.get_host_info.remove(id).unwrap_or_default(),
|
||||
))
|
||||
.filter(|cb| !cb.0.is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn add_get_ssl_certificate(
|
||||
&self,
|
||||
ctx: EffectContext,
|
||||
hostnames: BTreeSet<InternedString>,
|
||||
cert: FullchainCertData,
|
||||
algorithm: Algorithm,
|
||||
handler: CallbackHandler,
|
||||
) {
|
||||
self.mutate(|this| {
|
||||
this.get_ssl_certificate
|
||||
.entry((hostnames.clone(), cert.clone(), algorithm))
|
||||
.or_insert_with(|| {
|
||||
(
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = async {
|
||||
loop {
|
||||
match cert
|
||||
.expiration()
|
||||
.ok()
|
||||
.and_then(|e| e.duration_since(SystemTime::now()).ok())
|
||||
{
|
||||
Some(d) => {
|
||||
tokio::time::sleep(min(Duration::from_secs(86400), d))
|
||||
.await
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
let Ok(ctx) = ctx.deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some((_, callbacks)) =
|
||||
ctx.seed.ctx.callbacks.mutate(|this| {
|
||||
this.get_ssl_certificate
|
||||
.remove(&(hostnames, cert, algorithm))
|
||||
})
|
||||
{
|
||||
CallbackHandlers(callbacks).call(vector![]).await?;
|
||||
}
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Error in callback handler for getSslCertificate: {e}"
|
||||
);
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
})
|
||||
.into(),
|
||||
Vec::new(),
|
||||
)
|
||||
})
|
||||
.1
|
||||
.push(handler);
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn add_get_store(
|
||||
&self,
|
||||
package_id: PackageId,
|
||||
path: JsonPointer,
|
||||
handler: CallbackHandler,
|
||||
) {
|
||||
self.mutate(|this| {
|
||||
this.get_store
|
||||
.entry(package_id)
|
||||
.or_default()
|
||||
.entry(path)
|
||||
.or_default()
|
||||
.push(handler)
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_store(
|
||||
&self,
|
||||
package_id: &PackageId,
|
||||
path: &JsonPointer,
|
||||
) -> Option<CallbackHandlers> {
|
||||
self.mutate(|this| {
|
||||
if let Some(watched) = this.get_store.get_mut(package_id) {
|
||||
let mut res = Vec::new();
|
||||
watched.retain(|ptr, cbs| {
|
||||
if ptr.starts_with(path) || path.starts_with(ptr) {
|
||||
res.append(cbs);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
Some(CallbackHandlers(res))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.filter(|cb| !cb.0.is_empty())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CallbackHandler {
|
||||
handle: CallbackHandle,
|
||||
seed: Weak<ServiceActorSeed>,
|
||||
}
|
||||
impl CallbackHandler {
|
||||
pub fn new(service: &Service, handle: CallbackHandle) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
seed: Arc::downgrade(&service.seed),
|
||||
}
|
||||
}
|
||||
pub async fn call(mut self, args: Vector<Value>) -> Result<(), Error> {
|
||||
if let Some(seed) = self.seed.upgrade() {
|
||||
seed.persistent_container
|
||||
.callback(self.handle.take(), args)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Drop for CallbackHandler {
|
||||
fn drop(&mut self) {
|
||||
if self.handle.is_active() {
|
||||
warn!("Callback handler dropped while still active!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CallbackHandlers(Vec<CallbackHandler>);
|
||||
impl CallbackHandlers {
|
||||
pub async fn call(self, args: Vector<Value>) -> Result<(), Error> {
|
||||
let mut err = ErrorCollection::new();
|
||||
for res in join_all(self.0.into_iter().map(|cb| cb.call(args.clone()))).await {
|
||||
err.handle(res);
|
||||
}
|
||||
err.into_result()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clear_callbacks(context: EffectContext) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
context
|
||||
.seed
|
||||
.persistent_container
|
||||
.state
|
||||
.send_if_modified(|s| !std::mem::take(&mut s.callbacks).is_empty());
|
||||
context.seed.ctx.callbacks.gc();
|
||||
Ok(())
|
||||
}
|
||||
53
core/startos/src/service/effects/config.rs
Normal file
53
core/startos/src/service/effects/config.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use models::PackageId;
|
||||
|
||||
use crate::service::effects::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetConfiguredParams {
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
}
|
||||
pub async fn get_configured(context: EffectContext) -> Result<bool, Error> {
|
||||
let context = context.deref()?;
|
||||
let peeked = context.seed.ctx.db.peek().await;
|
||||
let package_id = &context.seed.id;
|
||||
peeked
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_idx(package_id)
|
||||
.or_not_found(package_id)?
|
||||
.as_status()
|
||||
.as_configured()
|
||||
.de()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SetConfigured {
|
||||
configured: bool,
|
||||
}
|
||||
pub async fn set_configured(
|
||||
context: EffectContext,
|
||||
SetConfigured { configured }: SetConfigured,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = &context.seed.id;
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(package_id)
|
||||
.or_not_found(package_id)?
|
||||
.as_status_mut()
|
||||
.as_configured_mut()
|
||||
.ser(&configured)
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
27
core/startos/src/service/effects/context.rs
Normal file
27
core/startos/src/service/effects/context.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use rpc_toolkit::Context;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::service::Service;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(in crate::service) struct EffectContext(Weak<Service>);
|
||||
impl EffectContext {
|
||||
pub fn new(service: Weak<Service>) -> Self {
|
||||
Self(service)
|
||||
}
|
||||
}
|
||||
impl Context for EffectContext {}
|
||||
impl EffectContext {
|
||||
pub(super) fn deref(&self) -> Result<Arc<Service>, Error> {
|
||||
if let Some(seed) = Weak::upgrade(&self.0) {
|
||||
Ok(seed)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("Service has already been destroyed"),
|
||||
ErrorKind::InvalidRequest,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
66
core/startos/src/service/effects/control.rs
Normal file
66
core/startos/src/service/effects/control.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::util::clap::FromStrParser;
|
||||
|
||||
pub async fn restart(
|
||||
context: EffectContext,
|
||||
ProcedureId { procedure_id }: ProcedureId,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
context.restart(procedure_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown(
|
||||
context: EffectContext,
|
||||
ProcedureId { procedure_id }: ProcedureId,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
context.stop(procedure_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub enum SetMainStatusStatus {
|
||||
Running,
|
||||
Stopped,
|
||||
}
|
||||
impl FromStr for SetMainStatusStatus {
|
||||
type Err = color_eyre::eyre::Report;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"running" => Ok(Self::Running),
|
||||
"stopped" => Ok(Self::Stopped),
|
||||
_ => Err(eyre!("unknown status {s}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ValueParserFactory for SetMainStatusStatus {
|
||||
type Parser = FromStrParser<Self>;
|
||||
fn value_parser() -> Self::Parser {
|
||||
FromStrParser::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SetMainStatus {
|
||||
status: SetMainStatusStatus,
|
||||
}
|
||||
pub async fn set_main_status(
|
||||
context: EffectContext,
|
||||
SetMainStatus { status }: SetMainStatus,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
match status {
|
||||
SetMainStatusStatus::Running => context.seed.started(),
|
||||
SetMainStatusStatus::Stopped => context.seed.stopped(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
371
core/startos/src/service/effects/dependency.rs
Normal file
371
core/startos/src/service/effects/dependency.rs
Normal file
@@ -0,0 +1,371 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
use exver::VersionRange;
|
||||
use itertools::Itertools;
|
||||
use models::{HealthCheckId, PackageId, VolumeId};
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
|
||||
use crate::db::model::package::{
|
||||
CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference,
|
||||
};
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::status::health_check::HealthCheckResult;
|
||||
use crate::util::clap::FromStrParser;
|
||||
|
||||
#[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,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MountParams {
|
||||
location: String,
|
||||
target: MountTarget,
|
||||
}
|
||||
pub async fn mount(
|
||||
context: EffectContext,
|
||||
MountParams {
|
||||
location,
|
||||
target:
|
||||
MountTarget {
|
||||
package_id,
|
||||
volume_id,
|
||||
subpath,
|
||||
readonly,
|
||||
},
|
||||
}: MountParams,
|
||||
) -> Result<(), Error> {
|
||||
// TODO
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn get_installed_packages(context: EffectContext) -> Result<Vec<PackageId>, Error> {
|
||||
context
|
||||
.deref()?
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.peek()
|
||||
.await
|
||||
.into_public()
|
||||
.into_package_data()
|
||||
.keys()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct ExposeForDependentsParams {
|
||||
#[ts(type = "string[]")]
|
||||
paths: Vec<JsonPointer>,
|
||||
}
|
||||
pub async fn expose_for_dependents(
|
||||
context: EffectContext,
|
||||
ExposeForDependentsParams { paths }: ExposeForDependentsParams,
|
||||
) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub enum DependencyKind {
|
||||
Exists,
|
||||
Running,
|
||||
}
|
||||
#[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!("unknown dependency kind {kind}"),
|
||||
ErrorKind::InvalidRequest,
|
||||
)),
|
||||
None => match rest {
|
||||
"r" | "running" => Ok(BTreeSet::new()),
|
||||
kind => Err(Error::new(
|
||||
eyre!("unknown dependency 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 {
|
||||
#[serde(default)]
|
||||
procedure_id: Guid,
|
||||
dependencies: Vec<DependencyRequirement>,
|
||||
}
|
||||
pub async fn set_dependencies(
|
||||
context: EffectContext,
|
||||
SetDependenciesParams {
|
||||
procedure_id,
|
||||
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 config_satisfied =
|
||||
if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await {
|
||||
context
|
||||
.dependency_config(
|
||||
procedure_id.clone(),
|
||||
dep_id.clone(),
|
||||
dep_service.get_config(procedure_id.clone()).await?.config,
|
||||
)
|
||||
.await?
|
||||
.is_none()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
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,
|
||||
config_satisfied,
|
||||
};
|
||||
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
|
||||
}
|
||||
|
||||
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()?;
|
||||
|
||||
data.0
|
||||
.into_iter()
|
||||
.map(|(id, current_dependency_info)| {
|
||||
let CurrentDependencyInfo {
|
||||
version_range,
|
||||
kind,
|
||||
..
|
||||
} = current_dependency_info;
|
||||
Ok::<_, Error>(match kind {
|
||||
CurrentDependencyKind::Exists => {
|
||||
DependencyRequirement::Exists { id, version_range }
|
||||
}
|
||||
CurrentDependencyKind::Running { health_checks } => {
|
||||
DependencyRequirement::Running {
|
||||
id,
|
||||
health_checks,
|
||||
version_range,
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.try_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,
|
||||
is_installed: bool,
|
||||
is_running: bool,
|
||||
config_satisfied: bool,
|
||||
health_checks: BTreeMap<HealthCheckId, HealthCheckResult>,
|
||||
#[ts(type = "string | null")]
|
||||
version: Option<exver::ExtendedVersion>,
|
||||
}
|
||||
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 current_dependencies = db
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_idx(&context.seed.id)
|
||||
.or_not_found(&context.seed.id)?
|
||||
.as_current_dependencies()
|
||||
.de()?;
|
||||
let package_ids: 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_ids.len());
|
||||
|
||||
for (package_id, dependency_info) in package_ids {
|
||||
let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else {
|
||||
results.push(CheckDependenciesResult {
|
||||
package_id,
|
||||
is_installed: false,
|
||||
is_running: false,
|
||||
config_satisfied: false,
|
||||
health_checks: Default::default(),
|
||||
version: None,
|
||||
});
|
||||
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 version = Some(installed_version.clone());
|
||||
if ![installed_version]
|
||||
.into_iter()
|
||||
.chain(satisfies.into_iter().map(|v| v.into_version()))
|
||||
.any(|v| v.satisfies(&dependency_info.version_range))
|
||||
{
|
||||
results.push(CheckDependenciesResult {
|
||||
package_id,
|
||||
is_installed: false,
|
||||
is_running: false,
|
||||
config_satisfied: false,
|
||||
health_checks: Default::default(),
|
||||
version,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let is_installed = true;
|
||||
let status = package.as_status().as_main().de()?;
|
||||
let is_running = if is_installed {
|
||||
status.running()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let health_checks =
|
||||
if let CurrentDependencyKind::Running { health_checks } = &dependency_info.kind {
|
||||
status
|
||||
.health()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|(id, _)| health_checks.contains(id))
|
||||
.collect()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
results.push(CheckDependenciesResult {
|
||||
package_id,
|
||||
is_installed,
|
||||
is_running,
|
||||
config_satisfied: dependency_info.config_satisfied,
|
||||
health_checks,
|
||||
version,
|
||||
});
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
46
core/startos/src/service/effects/health.rs
Normal file
46
core/startos/src/service/effects/health.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use models::HealthCheckId;
|
||||
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::status::health_check::HealthCheckResult;
|
||||
use crate::status::MainStatus;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SetHealth {
|
||||
id: HealthCheckId,
|
||||
#[serde(flatten)]
|
||||
result: HealthCheckResult,
|
||||
}
|
||||
pub async fn set_health(
|
||||
context: EffectContext,
|
||||
SetHealth { id, result }: SetHealth,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
|
||||
let package_id = &context.seed.id;
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(move |db| {
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(package_id)
|
||||
.or_not_found(package_id)?
|
||||
.as_status_mut()
|
||||
.as_main_mut()
|
||||
.mutate(|main| {
|
||||
match main {
|
||||
&mut MainStatus::Running { ref mut health, .. }
|
||||
| &mut MainStatus::BackingUp { ref mut health, .. } => {
|
||||
health.insert(id, result);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
163
core/startos/src/service/effects/image.rs
Normal file
163
core/startos/src/service/effects/image.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use std::ffi::OsString;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use models::ImageId;
|
||||
use rpc_toolkit::Context;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::disk::mount::filesystem::overlayfs::OverlayGuard;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::util::Invoke;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Parser)]
|
||||
pub struct ChrootParams {
|
||||
#[arg(short = 'e', long = "env")]
|
||||
env: Option<PathBuf>,
|
||||
#[arg(short = 'w', long = "workdir")]
|
||||
workdir: Option<PathBuf>,
|
||||
#[arg(short = 'u', long = "user")]
|
||||
user: Option<String>,
|
||||
path: PathBuf,
|
||||
command: OsString,
|
||||
args: Vec<OsString>,
|
||||
}
|
||||
pub fn chroot<C: Context>(
|
||||
_: C,
|
||||
ChrootParams {
|
||||
env,
|
||||
workdir,
|
||||
user,
|
||||
path,
|
||||
command,
|
||||
args,
|
||||
}: ChrootParams,
|
||||
) -> Result<(), Error> {
|
||||
let mut cmd = std::process::Command::new(command);
|
||||
if let Some(env) = env {
|
||||
for (k, v) in std::fs::read_to_string(env)?
|
||||
.lines()
|
||||
.map(|l| l.trim())
|
||||
.filter_map(|l| l.split_once("="))
|
||||
{
|
||||
cmd.env(k, v);
|
||||
}
|
||||
}
|
||||
nix::unistd::setsid().ok(); // https://stackoverflow.com/questions/25701333/os-setsid-operation-not-permitted
|
||||
std::os::unix::fs::chroot(path)?;
|
||||
if let Some(uid) = user.as_deref().and_then(|u| u.parse::<u32>().ok()) {
|
||||
cmd.uid(uid);
|
||||
} else if let Some(user) = user {
|
||||
let (uid, gid) = std::fs::read_to_string("/etc/passwd")?
|
||||
.lines()
|
||||
.find_map(|l| {
|
||||
let mut split = l.trim().split(":");
|
||||
if user != split.next()? {
|
||||
return None;
|
||||
}
|
||||
split.next(); // throw away x
|
||||
Some((split.next()?.parse().ok()?, split.next()?.parse().ok()?))
|
||||
// uid gid
|
||||
})
|
||||
.or_not_found(lazy_format!("{user} in /etc/passwd"))?;
|
||||
cmd.uid(uid);
|
||||
cmd.gid(gid);
|
||||
};
|
||||
if let Some(workdir) = workdir {
|
||||
cmd.current_dir(workdir);
|
||||
}
|
||||
cmd.args(args);
|
||||
Err(cmd.exec().into())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct DestroyOverlayedImageParams {
|
||||
guid: Guid,
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
pub async fn destroy_overlayed_image(
|
||||
context: EffectContext,
|
||||
DestroyOverlayedImageParams { guid }: DestroyOverlayedImageParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
if context
|
||||
.seed
|
||||
.persistent_container
|
||||
.overlays
|
||||
.lock()
|
||||
.await
|
||||
.remove(&guid)
|
||||
.is_none()
|
||||
{
|
||||
tracing::warn!("Could not find a guard to remove on the destroy overlayed image; assumming that it already is removed and will be skipping");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CreateOverlayedImageParams {
|
||||
image_id: ImageId,
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
pub async fn create_overlayed_image(
|
||||
context: EffectContext,
|
||||
CreateOverlayedImageParams { image_id }: CreateOverlayedImageParams,
|
||||
) -> Result<(PathBuf, Guid), Error> {
|
||||
let context = context.deref()?;
|
||||
if let Some(image) = context
|
||||
.seed
|
||||
.persistent_container
|
||||
.images
|
||||
.get(&image_id)
|
||||
.cloned()
|
||||
{
|
||||
let guid = Guid::new();
|
||||
let rootfs_dir = context
|
||||
.seed
|
||||
.persistent_container
|
||||
.lxc_container
|
||||
.get()
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("PersistentContainer has been destroyed"),
|
||||
ErrorKind::Incoherent,
|
||||
)
|
||||
})?
|
||||
.rootfs_dir();
|
||||
let mountpoint = rootfs_dir
|
||||
.join("media/startos/overlays")
|
||||
.join(guid.as_ref());
|
||||
tokio::fs::create_dir_all(&mountpoint).await?;
|
||||
let container_mountpoint = Path::new("/").join(
|
||||
mountpoint
|
||||
.strip_prefix(rootfs_dir)
|
||||
.with_kind(ErrorKind::Incoherent)?,
|
||||
);
|
||||
tracing::info!("Mounting overlay {guid} for {image_id}");
|
||||
let guard = OverlayGuard::mount(image, &mountpoint).await?;
|
||||
Command::new("chown")
|
||||
.arg("100000:100000")
|
||||
.arg(&mountpoint)
|
||||
.invoke(ErrorKind::Filesystem)
|
||||
.await?;
|
||||
tracing::info!("Mounted overlay {guid} for {image_id}");
|
||||
context
|
||||
.seed
|
||||
.persistent_container
|
||||
.overlays
|
||||
.lock()
|
||||
.await
|
||||
.insert(guid.clone(), guard);
|
||||
Ok((container_mountpoint, guid))
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("image {image_id} not found in s9pk"),
|
||||
ErrorKind::NotFound,
|
||||
))
|
||||
}
|
||||
}
|
||||
174
core/startos/src/service/effects/mod.rs
Normal file
174
core/startos/src/service/effects/mod.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use rpc_toolkit::{from_fn, from_fn_async, Context, HandlerExt, ParentHandler};
|
||||
|
||||
use crate::echo;
|
||||
use crate::prelude::*;
|
||||
use crate::service::cli::ContainerCliContext;
|
||||
use crate::service::effects::context::EffectContext;
|
||||
|
||||
mod action;
|
||||
pub mod callbacks;
|
||||
mod config;
|
||||
pub mod context;
|
||||
mod control;
|
||||
mod dependency;
|
||||
mod health;
|
||||
mod image;
|
||||
mod net;
|
||||
mod prelude;
|
||||
mod store;
|
||||
mod system;
|
||||
|
||||
pub fn handler<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand("gitInfo", from_fn(|_: C| crate::version::git_info()))
|
||||
.subcommand(
|
||||
"echo",
|
||||
from_fn(echo::<EffectContext>).with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
// action
|
||||
.subcommand(
|
||||
"executeAction",
|
||||
from_fn_async(action::execute_action).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"exportAction",
|
||||
from_fn_async(action::export_action).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"clearActions",
|
||||
from_fn_async(action::clear_actions).no_cli(),
|
||||
)
|
||||
// callbacks
|
||||
.subcommand(
|
||||
"clearCallbacks",
|
||||
from_fn(callbacks::clear_callbacks).no_cli(),
|
||||
)
|
||||
// config
|
||||
.subcommand(
|
||||
"getConfigured",
|
||||
from_fn_async(config::get_configured).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"setConfigured",
|
||||
from_fn_async(config::set_configured)
|
||||
.no_display()
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
// control
|
||||
.subcommand(
|
||||
"restart",
|
||||
from_fn_async(control::restart)
|
||||
.no_display()
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"shutdown",
|
||||
from_fn_async(control::shutdown)
|
||||
.no_display()
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"setMainStatus",
|
||||
from_fn_async(control::set_main_status)
|
||||
.no_display()
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
// dependency
|
||||
.subcommand(
|
||||
"setDependencies",
|
||||
from_fn_async(dependency::set_dependencies)
|
||||
.no_display()
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"getDependencies",
|
||||
from_fn_async(dependency::get_dependencies)
|
||||
.no_display()
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"checkDependencies",
|
||||
from_fn_async(dependency::check_dependencies)
|
||||
.no_display()
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
.subcommand("mount", from_fn_async(dependency::mount).no_cli())
|
||||
.subcommand(
|
||||
"getInstalledPackages",
|
||||
from_fn_async(dependency::get_installed_packages).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"exposeForDependents",
|
||||
from_fn_async(dependency::expose_for_dependents).no_cli(),
|
||||
)
|
||||
// health
|
||||
.subcommand("setHealth", from_fn_async(health::set_health).no_cli())
|
||||
// image
|
||||
.subcommand(
|
||||
"chroot",
|
||||
from_fn(image::chroot::<ContainerCliContext>).no_display(),
|
||||
)
|
||||
.subcommand(
|
||||
"createOverlayedImage",
|
||||
from_fn_async(image::create_overlayed_image)
|
||||
.with_custom_display_fn(|_, (path, _)| Ok(println!("{}", path.display())))
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"destroyOverlayedImage",
|
||||
from_fn_async(image::destroy_overlayed_image).no_cli(),
|
||||
)
|
||||
// net
|
||||
.subcommand("bind", from_fn_async(net::bind::bind).no_cli())
|
||||
.subcommand(
|
||||
"getServicePortForward",
|
||||
from_fn_async(net::bind::get_service_port_forward).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"clearBindings",
|
||||
from_fn_async(net::bind::clear_bindings).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"getHostInfo",
|
||||
from_fn_async(net::host::get_host_info).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"getPrimaryUrl",
|
||||
from_fn_async(net::host::get_primary_url).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"getContainerIp",
|
||||
from_fn_async(net::info::get_container_ip).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"exportServiceInterface",
|
||||
from_fn_async(net::interface::export_service_interface).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"getServiceInterface",
|
||||
from_fn_async(net::interface::get_service_interface).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"listServiceInterfaces",
|
||||
from_fn_async(net::interface::list_service_interfaces).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"clearServiceInterfaces",
|
||||
from_fn_async(net::interface::clear_service_interfaces).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"getSslCertificate",
|
||||
from_fn_async(net::ssl::get_ssl_certificate).no_cli(),
|
||||
)
|
||||
.subcommand("getSslKey", from_fn_async(net::ssl::get_ssl_key).no_cli())
|
||||
// store
|
||||
.subcommand("getStore", from_fn_async(store::get_store).no_cli())
|
||||
.subcommand("setStore", from_fn_async(store::set_store).no_cli())
|
||||
// system
|
||||
.subcommand(
|
||||
"getSystemSmtp",
|
||||
from_fn_async(system::get_system_smtp).no_cli(),
|
||||
)
|
||||
|
||||
// TODO Callbacks
|
||||
}
|
||||
56
core/startos/src/service/effects/net/bind.rs
Normal file
56
core/startos/src/service/effects/net/bind.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use models::{HostId, PackageId};
|
||||
|
||||
use crate::net::host::binding::{BindOptions, LanInfo};
|
||||
use crate::net::host::HostKind;
|
||||
use crate::service::effects::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct BindParams {
|
||||
kind: HostKind,
|
||||
id: HostId,
|
||||
internal_port: u16,
|
||||
#[serde(flatten)]
|
||||
options: BindOptions,
|
||||
}
|
||||
pub async fn bind(
|
||||
context: EffectContext,
|
||||
BindParams {
|
||||
kind,
|
||||
id,
|
||||
internal_port,
|
||||
options,
|
||||
}: BindParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let mut svc = context.seed.persistent_container.net_service.lock().await;
|
||||
svc.bind(kind, id, internal_port, options).await
|
||||
}
|
||||
|
||||
pub async fn clear_bindings(context: EffectContext) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let mut svc = context.seed.persistent_container.net_service.lock().await;
|
||||
svc.clear_bindings().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetServicePortForwardParams {
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
host_id: HostId,
|
||||
internal_port: u32,
|
||||
}
|
||||
pub async fn get_service_port_forward(
|
||||
context: EffectContext,
|
||||
data: GetServicePortForwardParams,
|
||||
) -> Result<LanInfo, Error> {
|
||||
let internal_port = data.internal_port as u16;
|
||||
|
||||
let context = context.deref()?;
|
||||
let net_service = context.seed.persistent_container.net_service.lock().await;
|
||||
net_service.get_lan_port(data.host_id, internal_port)
|
||||
}
|
||||
73
core/startos/src/service/effects/net/host.rs
Normal file
73
core/startos/src/service/effects/net/host.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use models::{HostId, PackageId};
|
||||
|
||||
use crate::net::host::address::HostAddress;
|
||||
use crate::net::host::Host;
|
||||
use crate::service::effects::callbacks::CallbackHandler;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::service::rpc::CallbackId;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetPrimaryUrlParams {
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
host_id: HostId,
|
||||
#[ts(optional)]
|
||||
callback: Option<CallbackId>,
|
||||
}
|
||||
pub async fn get_primary_url(
|
||||
context: EffectContext,
|
||||
GetPrimaryUrlParams {
|
||||
package_id,
|
||||
host_id,
|
||||
callback,
|
||||
}: GetPrimaryUrlParams,
|
||||
) -> Result<Option<HostAddress>, Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = package_id.unwrap_or_else(|| context.seed.id.clone());
|
||||
|
||||
Ok(None) // TODO
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetHostInfoParams {
|
||||
host_id: HostId,
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
#[ts(optional)]
|
||||
callback: Option<CallbackId>,
|
||||
}
|
||||
pub async fn get_host_info(
|
||||
context: EffectContext,
|
||||
GetHostInfoParams {
|
||||
host_id,
|
||||
package_id,
|
||||
callback,
|
||||
}: GetHostInfoParams,
|
||||
) -> Result<Option<Host>, Error> {
|
||||
let context = context.deref()?;
|
||||
let db = context.seed.ctx.db.peek().await;
|
||||
let package_id = package_id.unwrap_or_else(|| context.seed.id.clone());
|
||||
|
||||
let res = db
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_idx(&package_id)
|
||||
.and_then(|m| m.as_hosts().as_idx(&host_id))
|
||||
.map(|m| m.de())
|
||||
.transpose()?;
|
||||
|
||||
if let Some(callback) = callback {
|
||||
let callback = callback.register(&context.seed.persistent_container);
|
||||
context.seed.ctx.callbacks.add_get_host_info(
|
||||
package_id,
|
||||
host_id,
|
||||
CallbackHandler::new(&context, callback),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
9
core/startos/src/service/effects/net/info.rs
Normal file
9
core/startos/src/service/effects/net/info.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use crate::service::effects::prelude::*;
|
||||
|
||||
pub async fn get_container_ip(context: EffectContext) -> Result<Ipv4Addr, Error> {
|
||||
let context = context.deref()?;
|
||||
let net_service = context.seed.persistent_container.net_service.lock().await;
|
||||
Ok(net_service.get_ip())
|
||||
}
|
||||
188
core/startos/src/service/effects/net/interface.rs
Normal file
188
core/startos/src/service/effects/net/interface.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use imbl::vector;
|
||||
use models::{PackageId, ServiceInterfaceId};
|
||||
|
||||
use crate::net::service_interface::{AddressInfo, ServiceInterface, ServiceInterfaceType};
|
||||
use crate::service::effects::callbacks::CallbackHandler;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::service::rpc::CallbackId;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExportServiceInterfaceParams {
|
||||
id: ServiceInterfaceId,
|
||||
name: String,
|
||||
description: String,
|
||||
has_primary: bool,
|
||||
disabled: bool,
|
||||
masked: bool,
|
||||
address_info: AddressInfo,
|
||||
r#type: ServiceInterfaceType,
|
||||
}
|
||||
pub async fn export_service_interface(
|
||||
context: EffectContext,
|
||||
ExportServiceInterfaceParams {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
has_primary,
|
||||
disabled,
|
||||
masked,
|
||||
address_info,
|
||||
r#type,
|
||||
}: ExportServiceInterfaceParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = context.seed.id.clone();
|
||||
|
||||
let service_interface = ServiceInterface {
|
||||
id: id.clone(),
|
||||
name,
|
||||
description,
|
||||
has_primary,
|
||||
disabled,
|
||||
masked,
|
||||
address_info,
|
||||
interface_type: r#type,
|
||||
};
|
||||
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&package_id)
|
||||
.or_not_found(&package_id)?
|
||||
.as_service_interfaces_mut()
|
||||
.insert(&id, &service_interface)?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
if let Some(callbacks) = context
|
||||
.seed
|
||||
.ctx
|
||||
.callbacks
|
||||
.get_service_interface(&(package_id.clone(), id))
|
||||
{
|
||||
callbacks.call(vector![]).await?;
|
||||
}
|
||||
if let Some(callbacks) = context
|
||||
.seed
|
||||
.ctx
|
||||
.callbacks
|
||||
.list_service_interfaces(&package_id)
|
||||
{
|
||||
callbacks.call(vector![]).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetServiceInterfaceParams {
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
service_interface_id: ServiceInterfaceId,
|
||||
#[ts(optional)]
|
||||
callback: Option<CallbackId>,
|
||||
}
|
||||
pub async fn get_service_interface(
|
||||
context: EffectContext,
|
||||
GetServiceInterfaceParams {
|
||||
package_id,
|
||||
service_interface_id,
|
||||
callback,
|
||||
}: GetServiceInterfaceParams,
|
||||
) -> Result<Option<ServiceInterface>, Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = package_id.unwrap_or_else(|| context.seed.id.clone());
|
||||
let db = context.seed.ctx.db.peek().await;
|
||||
|
||||
let interface = db
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_idx(&package_id)
|
||||
.and_then(|m| m.as_service_interfaces().as_idx(&service_interface_id))
|
||||
.map(|m| m.de())
|
||||
.transpose()?;
|
||||
|
||||
if let Some(callback) = callback {
|
||||
let callback = callback.register(&context.seed.persistent_container);
|
||||
context.seed.ctx.callbacks.add_get_service_interface(
|
||||
package_id,
|
||||
service_interface_id,
|
||||
CallbackHandler::new(&context, callback),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(interface)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListServiceInterfacesParams {
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
#[ts(optional)]
|
||||
callback: Option<CallbackId>,
|
||||
}
|
||||
pub async fn list_service_interfaces(
|
||||
context: EffectContext,
|
||||
ListServiceInterfacesParams {
|
||||
package_id,
|
||||
callback,
|
||||
}: ListServiceInterfacesParams,
|
||||
) -> Result<BTreeMap<ServiceInterfaceId, ServiceInterface>, Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = package_id.unwrap_or_else(|| context.seed.id.clone());
|
||||
|
||||
let res = context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.peek()
|
||||
.await
|
||||
.into_public()
|
||||
.into_package_data()
|
||||
.into_idx(&package_id)
|
||||
.map(|m| m.into_service_interfaces().de())
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(callback) = callback {
|
||||
let callback = callback.register(&context.seed.persistent_container);
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.callbacks
|
||||
.add_list_service_interfaces(package_id, CallbackHandler::new(&context, callback));
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn clear_service_interfaces(context: EffectContext) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = context.seed.id.clone();
|
||||
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&package_id)
|
||||
.or_not_found(&package_id)?
|
||||
.as_service_interfaces_mut()
|
||||
.ser(&Default::default())
|
||||
})
|
||||
.await
|
||||
}
|
||||
5
core/startos/src/service/effects/net/mod.rs
Normal file
5
core/startos/src/service/effects/net/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod bind;
|
||||
pub mod host;
|
||||
pub mod info;
|
||||
pub mod interface;
|
||||
pub mod ssl;
|
||||
169
core/startos/src/service/effects/net/ssl.rs
Normal file
169
core/startos/src/service/effects/net/ssl.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
|
||||
use crate::service::effects::callbacks::CallbackHandler;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::service::rpc::CallbackId;
|
||||
use crate::util::serde::Pem;
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, TS, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub enum Algorithm {
|
||||
Ecdsa,
|
||||
Ed25519,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetSslCertificateParams {
|
||||
#[ts(type = "string[]")]
|
||||
hostnames: BTreeSet<InternedString>,
|
||||
#[ts(optional)]
|
||||
algorithm: Option<Algorithm>, //"ecdsa" | "ed25519"
|
||||
#[ts(optional)]
|
||||
callback: Option<CallbackId>,
|
||||
}
|
||||
pub async fn get_ssl_certificate(
|
||||
ctx: EffectContext,
|
||||
GetSslCertificateParams {
|
||||
hostnames,
|
||||
algorithm,
|
||||
callback,
|
||||
}: GetSslCertificateParams,
|
||||
) -> Result<Vec<String>, Error> {
|
||||
let context = ctx.deref()?;
|
||||
let algorithm = algorithm.unwrap_or(Algorithm::Ecdsa);
|
||||
|
||||
let cert = context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let errfn = |h: &str| Error::new(eyre!("unknown hostname: {h}"), ErrorKind::NotFound);
|
||||
let entries = db.as_public().as_package_data().as_entries()?;
|
||||
let packages = entries.iter().map(|(k, _)| k).collect::<BTreeSet<_>>();
|
||||
let allowed_hostnames = entries
|
||||
.iter()
|
||||
.map(|(_, m)| m.as_hosts().as_entries())
|
||||
.flatten_ok()
|
||||
.map_ok(|(_, m)| m.as_addresses().de())
|
||||
.map(|a| a.and_then(|a| a))
|
||||
.flatten_ok()
|
||||
.map_ok(|a| InternedString::from_display(&a))
|
||||
.try_collect::<_, BTreeSet<_>, _>()?;
|
||||
for hostname in &hostnames {
|
||||
if let Some(internal) = hostname
|
||||
.strip_suffix(".embassy")
|
||||
.or_else(|| hostname.strip_suffix(".startos"))
|
||||
{
|
||||
if !packages.contains(internal) {
|
||||
return Err(errfn(&*hostname));
|
||||
}
|
||||
} else {
|
||||
if !allowed_hostnames.contains(hostname) {
|
||||
return Err(errfn(&*hostname));
|
||||
}
|
||||
}
|
||||
}
|
||||
db.as_private_mut()
|
||||
.as_key_store_mut()
|
||||
.as_local_certs_mut()
|
||||
.cert_for(&hostnames)
|
||||
})
|
||||
.await?;
|
||||
let fullchain = match algorithm {
|
||||
Algorithm::Ecdsa => cert.fullchain_nistp256(),
|
||||
Algorithm::Ed25519 => cert.fullchain_ed25519(),
|
||||
};
|
||||
|
||||
let res = fullchain
|
||||
.into_iter()
|
||||
.map(|c| c.to_pem())
|
||||
.map_ok(String::from_utf8)
|
||||
.map(|a| Ok::<_, Error>(a??))
|
||||
.try_collect()?;
|
||||
|
||||
if let Some(callback) = callback {
|
||||
let callback = callback.register(&context.seed.persistent_container);
|
||||
context.seed.ctx.callbacks.add_get_ssl_certificate(
|
||||
ctx,
|
||||
hostnames,
|
||||
cert,
|
||||
algorithm,
|
||||
CallbackHandler::new(&context, callback),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetSslKeyParams {
|
||||
#[ts(type = "string[]")]
|
||||
hostnames: BTreeSet<InternedString>,
|
||||
#[ts(optional)]
|
||||
algorithm: Option<Algorithm>, //"ecdsa" | "ed25519"
|
||||
}
|
||||
pub async fn get_ssl_key(
|
||||
context: EffectContext,
|
||||
GetSslKeyParams {
|
||||
hostnames,
|
||||
algorithm,
|
||||
}: GetSslKeyParams,
|
||||
) -> Result<Pem<PKey<Private>>, Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = &context.seed.id;
|
||||
let algorithm = algorithm.unwrap_or(Algorithm::Ecdsa);
|
||||
|
||||
let cert = context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let errfn = |h: &str| Error::new(eyre!("unknown hostname: {h}"), ErrorKind::NotFound);
|
||||
let allowed_hostnames = db
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_idx(package_id)
|
||||
.into_iter()
|
||||
.map(|m| m.as_hosts().as_entries())
|
||||
.flatten_ok()
|
||||
.map_ok(|(_, m)| m.as_addresses().de())
|
||||
.map(|a| a.and_then(|a| a))
|
||||
.flatten_ok()
|
||||
.map_ok(|a| InternedString::from_display(&a))
|
||||
.try_collect::<_, BTreeSet<_>, _>()?;
|
||||
for hostname in &hostnames {
|
||||
if let Some(internal) = hostname
|
||||
.strip_suffix(".embassy")
|
||||
.or_else(|| hostname.strip_suffix(".startos"))
|
||||
{
|
||||
if internal != &**package_id {
|
||||
return Err(errfn(&*hostname));
|
||||
}
|
||||
} else {
|
||||
if !allowed_hostnames.contains(hostname) {
|
||||
return Err(errfn(&*hostname));
|
||||
}
|
||||
}
|
||||
}
|
||||
db.as_private_mut()
|
||||
.as_key_store_mut()
|
||||
.as_local_certs_mut()
|
||||
.cert_for(&hostnames)
|
||||
})
|
||||
.await?;
|
||||
let key = match algorithm {
|
||||
Algorithm::Ecdsa => cert.leaf.keys.nistp256,
|
||||
Algorithm::Ed25519 => cert.leaf.keys.ed25519,
|
||||
};
|
||||
|
||||
Ok(Pem(key))
|
||||
}
|
||||
16
core/startos/src/service/effects/prelude.rs
Normal file
16
core/startos/src/service/effects/prelude.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
pub use clap::Parser;
|
||||
pub use serde::{Deserialize, Serialize};
|
||||
pub use ts_rs::TS;
|
||||
|
||||
pub use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
pub(super) use crate::service::effects::context::EffectContext;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct ProcedureId {
|
||||
#[serde(default)]
|
||||
#[arg(default_value_t, long)]
|
||||
pub procedure_id: Guid,
|
||||
}
|
||||
93
core/startos/src/service/effects/store.rs
Normal file
93
core/startos/src/service/effects/store.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use imbl::vector;
|
||||
use imbl_value::json;
|
||||
use models::PackageId;
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
|
||||
use crate::service::effects::callbacks::CallbackHandler;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::service::rpc::CallbackId;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetStoreParams {
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
#[ts(type = "string")]
|
||||
path: JsonPointer,
|
||||
#[ts(optional)]
|
||||
callback: Option<CallbackId>,
|
||||
}
|
||||
pub async fn get_store(
|
||||
context: EffectContext,
|
||||
GetStoreParams {
|
||||
package_id,
|
||||
path,
|
||||
callback,
|
||||
}: GetStoreParams,
|
||||
) -> Result<Value, Error> {
|
||||
let context = context.deref()?;
|
||||
let peeked = context.seed.ctx.db.peek().await;
|
||||
let package_id = package_id.unwrap_or(context.seed.id.clone());
|
||||
let value = peeked
|
||||
.as_private()
|
||||
.as_package_stores()
|
||||
.as_idx(&package_id)
|
||||
.or_not_found(&package_id)?
|
||||
.de()?;
|
||||
|
||||
if let Some(callback) = callback {
|
||||
let callback = callback.register(&context.seed.persistent_container);
|
||||
context.seed.ctx.callbacks.add_get_store(
|
||||
package_id,
|
||||
path.clone(),
|
||||
CallbackHandler::new(&context, callback),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(path
|
||||
.get(&value)
|
||||
.ok_or_else(|| Error::new(eyre!("Did not find value at path"), ErrorKind::NotFound))?
|
||||
.clone())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SetStoreParams {
|
||||
#[ts(type = "any")]
|
||||
value: Value,
|
||||
#[ts(type = "string")]
|
||||
path: JsonPointer,
|
||||
}
|
||||
pub async fn set_store(
|
||||
context: EffectContext,
|
||||
SetStoreParams { value, path }: SetStoreParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = &context.seed.id;
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let model = db
|
||||
.as_private_mut()
|
||||
.as_package_stores_mut()
|
||||
.upsert(package_id, || Ok(json!({})))?;
|
||||
let mut model_value = model.de()?;
|
||||
if model_value.is_null() {
|
||||
model_value = json!({});
|
||||
}
|
||||
path.set(&mut model_value, value, true)
|
||||
.with_kind(ErrorKind::ParseDbField)?;
|
||||
model.ser(&model_value)
|
||||
})
|
||||
.await?;
|
||||
|
||||
if let Some(callbacks) = context.seed.ctx.callbacks.get_store(package_id, &path) {
|
||||
callbacks.call(vector![]).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
39
core/startos/src/service/effects/system.rs
Normal file
39
core/startos/src/service/effects/system.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use crate::service::effects::callbacks::CallbackHandler;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::service::rpc::CallbackId;
|
||||
use crate::system::SmtpValue;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetSystemSmtpParams {
|
||||
#[arg(skip)]
|
||||
callback: Option<CallbackId>,
|
||||
}
|
||||
pub async fn get_system_smtp(
|
||||
context: EffectContext,
|
||||
GetSystemSmtpParams { callback }: GetSystemSmtpParams,
|
||||
) -> Result<Option<SmtpValue>, Error> {
|
||||
let context = context.deref()?;
|
||||
let res = context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.peek()
|
||||
.await
|
||||
.into_public()
|
||||
.into_server_info()
|
||||
.into_smtp()
|
||||
.de()?;
|
||||
|
||||
if let Some(callback) = callback {
|
||||
let callback = callback.register(&context.seed.persistent_container);
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.callbacks
|
||||
.add_get_system_smtp(CallbackHandler::new(&context, callback));
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
@@ -27,10 +27,7 @@ use crate::progress::{NamedProgress, Progress};
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::service::service_map::InstallProgressHandles;
|
||||
use crate::service::transition::TransitionKind;
|
||||
use crate::status::health_check::HealthCheckResult;
|
||||
use crate::status::MainStatus;
|
||||
use crate::util::actor::background::BackgroundJobQueue;
|
||||
use crate::util::actor::concurrent::ConcurrentActor;
|
||||
use crate::util::actor::Actor;
|
||||
use crate::util::io::create_file;
|
||||
@@ -43,11 +40,11 @@ pub mod cli;
|
||||
mod config;
|
||||
mod control;
|
||||
mod dependencies;
|
||||
pub mod effects;
|
||||
pub mod persistent_container;
|
||||
mod properties;
|
||||
mod rpc;
|
||||
mod service_actor;
|
||||
pub mod service_effect_handler;
|
||||
pub mod service_map;
|
||||
mod start_stop;
|
||||
mod transition;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
@@ -6,6 +6,7 @@ use std::time::Duration;
|
||||
use futures::future::ready;
|
||||
use futures::{Future, FutureExt};
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use imbl::Vector;
|
||||
use models::{ImageId, ProcedureName, VolumeId};
|
||||
use rpc_toolkit::{Empty, Server, ShutdownHandle};
|
||||
use serde::de::DeserializeOwned;
|
||||
@@ -13,8 +14,6 @@ use tokio::process::Command;
|
||||
use tokio::sync::{oneshot, watch, Mutex, OnceCell};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::service_effect_handler::{service_effect_handler, EffectContext};
|
||||
use super::transition::{TransitionKind, TransitionState};
|
||||
use crate::context::RpcContext;
|
||||
use crate::disk::mount::filesystem::bind::Bind;
|
||||
use crate::disk::mount::filesystem::idmapped::IdMapped;
|
||||
@@ -28,7 +27,11 @@ use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::s9pk::merkle_archive::source::FileSource;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::service::effects::context::EffectContext;
|
||||
use crate::service::effects::handler;
|
||||
use crate::service::rpc::{CallbackHandle, CallbackId, CallbackParams};
|
||||
use crate::service::start_stop::StartStop;
|
||||
use crate::service::transition::{TransitionKind, TransitionState};
|
||||
use crate::service::{rpc, RunningStatus, Service};
|
||||
use crate::util::io::create_file;
|
||||
use crate::util::rpc_client::UnixRpcClient;
|
||||
@@ -42,6 +45,8 @@ const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
pub struct ServiceState {
|
||||
// This contains the start time and health check information for when the service is running. Note: Will be overwritting to the db,
|
||||
pub(super) running_status: Option<RunningStatus>,
|
||||
// This tracks references to callbacks registered by the running service:
|
||||
pub(super) callbacks: BTreeSet<Arc<CallbackId>>,
|
||||
/// Setting this value causes the service actor to try to bring the service to the specified state. This is done in the background job created in ServiceActor::init
|
||||
pub(super) desired_state: StartStop,
|
||||
/// Override the current desired state for the service during a transition (this is protected by a guard that sets this value to null on drop)
|
||||
@@ -61,6 +66,7 @@ impl ServiceState {
|
||||
pub fn new(desired_state: StartStop) -> Self {
|
||||
Self {
|
||||
running_status: Default::default(),
|
||||
callbacks: Default::default(),
|
||||
temp_desired_state: Default::default(),
|
||||
transition_state: Default::default(),
|
||||
desired_state,
|
||||
@@ -308,10 +314,7 @@ impl PersistentContainer {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn init(&self, seed: Weak<Service>) -> Result<(), Error> {
|
||||
let socket_server_context = EffectContext::new(seed);
|
||||
let server = Server::new(
|
||||
move || ready(Ok(socket_server_context.clone())),
|
||||
service_effect_handler(),
|
||||
);
|
||||
let server = Server::new(move || ready(Ok(socket_server_context.clone())), handler());
|
||||
let path = self
|
||||
.lxc_container
|
||||
.get()
|
||||
@@ -430,21 +433,13 @@ impl PersistentContainer {
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn start(&self) -> Result<(), Error> {
|
||||
self.execute(
|
||||
Guid::new(),
|
||||
ProcedureName::StartMain,
|
||||
Value::Null,
|
||||
Some(Duration::from_secs(5)), // TODO
|
||||
)
|
||||
.await?;
|
||||
self.rpc_client.request(rpc::Start, Empty {}).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn stop(&self) -> Result<(), Error> {
|
||||
let timeout: Option<crate::util::serde::Duration> = self
|
||||
.execute(Guid::new(), ProcedureName::StopMain, Value::Null, None)
|
||||
.await?;
|
||||
self.rpc_client.request(rpc::Stop, Empty {}).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -480,6 +475,19 @@ impl PersistentContainer {
|
||||
.and_then(from_value)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn callback(&self, handle: CallbackHandle, args: Vector<Value>) -> Result<(), Error> {
|
||||
let mut params = None;
|
||||
self.state.send_if_modified(|s| {
|
||||
params = handle.params(&mut s.callbacks, args);
|
||||
params.is_some()
|
||||
});
|
||||
if let Some(params) = params {
|
||||
self._callback(params).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn _execute(
|
||||
&self,
|
||||
@@ -523,6 +531,12 @@ impl PersistentContainer {
|
||||
fut.await?
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn _callback(&self, params: CallbackParams) -> Result<(), Error> {
|
||||
self.rpc_client.notify(rpc::Callback, params).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PersistentContainer {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
use imbl::Vector;
|
||||
use imbl_value::Value;
|
||||
use models::ProcedureName;
|
||||
use rpc_toolkit::yajrc::RpcMethod;
|
||||
@@ -8,6 +11,8 @@ use ts_rs::TS;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::persistent_container::PersistentContainer;
|
||||
use crate::util::Never;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Init;
|
||||
@@ -27,6 +32,42 @@ impl serde::Serialize for Init {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Start;
|
||||
impl RpcMethod for Start {
|
||||
type Params = Empty;
|
||||
type Response = ();
|
||||
fn as_str<'a>(&'a self) -> &'a str {
|
||||
"start"
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for Start {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Stop;
|
||||
impl RpcMethod for Stop {
|
||||
type Params = Empty;
|
||||
type Response = ();
|
||||
fn as_str<'a>(&'a self) -> &'a str {
|
||||
"stop"
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for Stop {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Exit;
|
||||
impl RpcMethod for Exit {
|
||||
@@ -104,3 +145,74 @@ impl serde::Serialize for Sandbox {
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, serde::Deserialize, serde::Serialize, TS, PartialEq, Eq, PartialOrd, Ord,
|
||||
)]
|
||||
#[ts(type = "number")]
|
||||
pub struct CallbackId(u64);
|
||||
impl CallbackId {
|
||||
pub fn register(self, container: &PersistentContainer) -> CallbackHandle {
|
||||
let this = Arc::new(self);
|
||||
let res = Arc::downgrade(&this);
|
||||
container
|
||||
.state
|
||||
.send_if_modified(|s| s.callbacks.insert(this));
|
||||
CallbackHandle(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CallbackHandle(Weak<CallbackId>);
|
||||
impl CallbackHandle {
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.0.strong_count() > 0
|
||||
}
|
||||
pub fn params(
|
||||
self,
|
||||
registered: &mut BTreeSet<Arc<CallbackId>>,
|
||||
args: Vector<Value>,
|
||||
) -> Option<CallbackParams> {
|
||||
if let Some(id) = self.0.upgrade() {
|
||||
if let Some(strong) = registered.get(&id) {
|
||||
if Arc::ptr_eq(strong, &id) {
|
||||
registered.remove(&id);
|
||||
return Some(CallbackParams::new(&*id, args));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn take(&mut self) -> Self {
|
||||
Self(std::mem::take(&mut self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Deserialize, serde::Serialize, TS)]
|
||||
pub struct CallbackParams {
|
||||
id: u64,
|
||||
#[ts(type = "any[]")]
|
||||
args: Vector<Value>,
|
||||
}
|
||||
impl CallbackParams {
|
||||
fn new(id: &CallbackId, args: Vector<Value>) -> Self {
|
||||
Self { id: id.0, args }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Callback;
|
||||
impl RpcMethod for Callback {
|
||||
type Params = CallbackParams;
|
||||
type Response = Never;
|
||||
fn as_str<'a>(&'a self) -> &'a str {
|
||||
"callback"
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for Callback {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::FutureExt;
|
||||
use imbl::vector;
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use tokio::process::Command;
|
||||
@@ -824,6 +825,51 @@ async fn get_disk_info() -> Result<MetricsDisk, Error> {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SmtpValue {
|
||||
#[arg(long)]
|
||||
pub server: String,
|
||||
#[arg(long)]
|
||||
pub port: u16,
|
||||
#[arg(long)]
|
||||
pub from: String,
|
||||
#[arg(long)]
|
||||
pub login: String,
|
||||
#[arg(long)]
|
||||
pub password: Option<String>,
|
||||
}
|
||||
pub async fn set_system_smtp(ctx: RpcContext, smtp: SmtpValue) -> Result<(), Error> {
|
||||
let smtp = Some(smtp);
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_smtp_mut()
|
||||
.ser(&smtp)
|
||||
})
|
||||
.await?;
|
||||
if let Some(callbacks) = ctx.callbacks.get_system_smtp() {
|
||||
callbacks.call(vector![to_value(&smtp)?]).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub async fn clear_system_smtp(ctx: RpcContext) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_smtp_mut()
|
||||
.ser(&None)
|
||||
})
|
||||
.await?;
|
||||
if let Some(callbacks) = ctx.callbacks.get_system_smtp() {
|
||||
callbacks.call(vector![Value::Null]).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
pub async fn test_get_temp() {
|
||||
|
||||
1213
core/startos/src/util/collections/eq_map.rs
Normal file
1213
core/startos/src/util/collections/eq_map.rs
Normal file
File diff suppressed because it is too large
Load Diff
3
core/startos/src/util/collections/mod.rs
Normal file
3
core/startos/src/util/collections/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod eq_map;
|
||||
|
||||
pub use eq_map::EqMap;
|
||||
@@ -34,8 +34,10 @@ use crate::shutdown::Shutdown;
|
||||
use crate::util::io::create_file;
|
||||
use crate::util::serde::{deserialize_from_str, serialize_display};
|
||||
use crate::{Error, ErrorKind, ResultExt as _};
|
||||
|
||||
pub mod actor;
|
||||
pub mod clap;
|
||||
pub mod collections;
|
||||
pub mod cpupower;
|
||||
pub mod crypto;
|
||||
pub mod future;
|
||||
|
||||
@@ -138,6 +138,31 @@ impl RpcClient {
|
||||
err.data = Some(json!("RpcClient thread has terminated"));
|
||||
Err(err)
|
||||
}
|
||||
|
||||
pub async fn notify<T: RpcMethod>(
|
||||
&mut self,
|
||||
method: T,
|
||||
params: T::Params,
|
||||
) -> Result<(), RpcError>
|
||||
where
|
||||
T: Serialize,
|
||||
T::Params: Serialize,
|
||||
{
|
||||
let request = RpcRequest {
|
||||
id: None,
|
||||
method,
|
||||
params,
|
||||
};
|
||||
self.writer
|
||||
.write_all((dbg!(serde_json::to_string(&request))? + "\n").as_bytes())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let mut err = rpc_toolkit::yajrc::INTERNAL_ERROR.clone();
|
||||
err.data = Some(json!(e.to_string()));
|
||||
err
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -224,4 +249,36 @@ impl UnixRpcClient {
|
||||
};
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn notify<T: RpcMethod>(&self, method: T, params: T::Params) -> Result<(), RpcError>
|
||||
where
|
||||
T: Serialize + Clone,
|
||||
T::Params: Serialize + Clone,
|
||||
{
|
||||
let mut tries = 0;
|
||||
let res = loop {
|
||||
let mut client = self.pool.clone().get().await?;
|
||||
if client.handler.is_finished() {
|
||||
client.destroy();
|
||||
continue;
|
||||
}
|
||||
let res = client.notify(method.clone(), params.clone()).await;
|
||||
match &res {
|
||||
Err(e) if e.code == rpc_toolkit::yajrc::INTERNAL_ERROR.code => {
|
||||
let mut e = Error::from(e.clone());
|
||||
e.kind = ErrorKind::Filesystem;
|
||||
tracing::error!("{e}");
|
||||
tracing::debug!("{e:?}");
|
||||
client.destroy();
|
||||
}
|
||||
_ => break res,
|
||||
}
|
||||
tries += 1;
|
||||
if tries > MAX_TRIES {
|
||||
tracing::warn!("Max Tries exceeded");
|
||||
break res;
|
||||
}
|
||||
};
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user