domains api + migration

This commit is contained in:
Aiden McClelland
2025-08-06 14:29:35 -06:00
parent ea12251a7e
commit d6dfaf8feb
39 changed files with 496 additions and 87 deletions

View File

@@ -14,7 +14,7 @@ keywords = [
name = "start-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.9" # VERSION_BUMP
version = "0.4.0-alpha.10" # VERSION_BUMP
license = "MIT"
[lib]
@@ -228,7 +228,8 @@ tracing-error = "0.2.0"
tracing-futures = "0.2.5"
tracing-journald = "0.3.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
trust-dns-server = "0.23.1"
trust-dns-server = "0.23.2"
trust-dns-client = "0.23.2"
ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-as" } # "8.1.0"
typed-builder = "0.21.0"
unix-named-pipe = "0.2.0"

View File

@@ -12,7 +12,7 @@ use tracing::instrument;
use crate::context::config::ServerConfig;
use crate::context::rpc::InitRpcContextPhases;
use crate::context::{DiagnosticContext, InitContext, RpcContext};
use crate::net::network_interface::SelfContainedNetworkInterfaceListener;
use crate::net::gateway::SelfContainedNetworkInterfaceListener;
use crate::net::web_server::{Acceptor, UpgradableListener, WebServer};
use crate::shutdown::Shutdown;
use crate::system::launch_metrics_task;

View File

@@ -92,8 +92,9 @@ impl Public {
enabled: true,
..Default::default()
},
network_interfaces: OrdMap::new(),
gateways: OrdMap::new(),
acme: BTreeMap::new(),
domains: BTreeMap::new(),
},
status_info: ServerStatus {
backup_progress: None,
@@ -191,9 +192,12 @@ pub struct NetworkInfo {
pub host: Host,
#[ts(as = "BTreeMap::<GatewayId, NetworkInterfaceInfo>")]
#[serde(default)]
pub network_interfaces: OrdMap<GatewayId, NetworkInterfaceInfo>,
pub gateways: OrdMap<GatewayId, NetworkInterfaceInfo>,
#[serde(default)]
pub acme: BTreeMap<AcmeProvider, AcmeSettings>,
#[serde(default)]
#[ts(as = "BTreeMap::<String, DomainSettings>")]
pub domains: BTreeMap<InternedString, DomainSettings>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)]
@@ -303,6 +307,14 @@ pub struct AcmeSettings {
pub contact: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct DomainSettings {
pub gateway: GatewayId,
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[model = "Model<Self>"]
#[ts(export)]

View File

@@ -150,7 +150,7 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
)
.subcommand(
"net",
net::net::<C>().with_about("Network commands related to tor and dhcp"),
net::net_api::<C>().with_about("Network commands related to tor and dhcp"),
)
.subcommand(
"auth",

View File

@@ -174,7 +174,7 @@ impl<'a> async_acme::cache::AcmeCache for AcmeCertCache<'a> {
}
}
pub fn acme<C: Context>() -> ParentHandler<C> {
pub fn acme_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"init",

View File

@@ -0,0 +1,204 @@
use std::collections::BTreeMap;
use clap::Parser;
use futures::TryFutureExt;
use helpers::NonDetachingJoinHandle;
use imbl_value::InternedString;
use models::GatewayId;
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use crate::context::{CliContext, RpcContext};
use crate::db::model::public::DomainSettings;
use crate::prelude::*;
use crate::util::new_guid;
use crate::util::serde::{display_serializable, HandlerExtSerde};
pub fn domain_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"list",
from_fn_async(list)
.with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, res);
}
let mut table = Table::new();
table.add_row(row![bc => "DOMAIN", "GATEWAY"]);
for (domain, info) in res {
table.add_row(row![domain, info.gateway]);
}
table.print_tty(false)?;
Ok(())
})
.with_about("List domains available to StartOS")
.with_call_remote::<CliContext>(),
)
.subcommand(
"add",
from_fn_async(add)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Add a domain for use with StartOS")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Remove a domain for use with StartOS")
.with_call_remote::<CliContext>(),
)
.subcommand(
"test-dns",
from_fn_async(test_dns)
.with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, res);
}
let mut table = Table::new();
table.add_row(row![bc -> "ROOT", if res.root { "✅️" } else { "❌️" }]);
table.add_row(row![bc -> "WILDCARD", if res.wildcard { "✅️" } else { "❌️" }]);
table.print_tty(false)?;
Ok(())
})
.with_about("Test the DNS configuration for a domain"),
)
}
pub async fn list(ctx: RpcContext) -> Result<BTreeMap<InternedString, DomainSettings>, Error> {
ctx.db
.peek()
.await
.into_public()
.into_server_info()
.into_network()
.into_domains()
.de()
}
#[derive(Deserialize, Serialize, Parser)]
pub struct AddDomainParams {
pub fqdn: InternedString,
pub gateway: GatewayId,
}
pub async fn add(
ctx: RpcContext,
AddDomainParams { fqdn, gateway }: AddDomainParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_public_mut()
.as_server_info_mut()
.as_network_mut()
.as_domains_mut()
.insert(&fqdn, &DomainSettings { gateway })
})
.await
.result?;
Ok(())
}
#[derive(Deserialize, Serialize, Parser)]
pub struct RemoveDomainParams {
pub fqdn: InternedString,
}
pub async fn remove(
ctx: RpcContext,
RemoveDomainParams { fqdn }: RemoveDomainParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_public_mut()
.as_server_info_mut()
.as_network_mut()
.as_domains_mut()
.remove(&fqdn)
})
.await
.result?;
Ok(())
}
#[derive(Deserialize, Serialize)]
pub struct TestDnsResult {
pub root: bool,
pub wildcard: bool,
}
pub async fn test_dns(
ctx: RpcContext,
AddDomainParams { fqdn, ref gateway }: AddDomainParams,
) -> Result<TestDnsResult, Error> {
use tokio::net::UdpSocket;
use trust_dns_client::client::{AsyncClient, ClientHandle};
use trust_dns_client::op::DnsResponse;
use trust_dns_client::proto::error::ProtoError;
use trust_dns_client::rr::{DNSClass, Name, RecordType};
use trust_dns_client::udp::UdpClientStream;
let wan_ip = ctx
.db
.peek()
.await
.into_public()
.into_server_info()
.into_network()
.into_gateways()
.into_idx(&gateway)
.or_not_found(&gateway)?
.into_ip_info()
.transpose()
.and_then(|i| i.into_wan_ip().transpose())
.or_not_found(lazy_format!("WAN IP for {gateway}"))?
.de()?;
let stream = UdpClientStream::<UdpSocket>::new(([127, 0, 0, 53], 53).into());
let (mut client, bg) = AsyncClient::connect(stream.map_err(ProtoError::from))
.await
.with_kind(ErrorKind::Network)?;
let bg: NonDetachingJoinHandle<_> = tokio::spawn(bg).into();
let root = fqdn.parse::<Name>().with_kind(ErrorKind::Network)?;
let wildcard = new_guid()
.parse::<Name>()
.with_kind(ErrorKind::Network)?
.append_domain(&root)
.with_kind(ErrorKind::Network)?;
let q_root = client
.query(root, DNSClass::IN, RecordType::A)
.await
.with_kind(ErrorKind::Network)?;
let q_wildcard = client
.query(wildcard, DNSClass::IN, RecordType::A)
.await
.with_kind(ErrorKind::Network)?;
bg.abort();
let check_q = |q: DnsResponse| {
q.answers().iter().any(|a| {
a.data()
.and_then(|d| d.as_a())
.map_or(false, |d| d.0 == wan_ip)
})
};
Ok(TestDnsResult {
root: check_q(q_root),
wildcard: check_q(q_wildcard),
})
}

View File

@@ -12,7 +12,7 @@ use tokio::process::Command;
use tokio::sync::mpsc;
use crate::db::model::public::NetworkInterfaceInfo;
use crate::net::network_interface::{DynInterfaceFilter, InterfaceFilter};
use crate::net::gateway::{DynInterfaceFilter, InterfaceFilter};
use crate::net::utils::ipv6_is_link_local;
use crate::prelude::*;
use crate::util::sync::Watch;

View File

@@ -32,7 +32,7 @@ use crate::context::{CliContext, RpcContext};
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
use crate::db::model::Database;
use crate::net::forward::START9_BRIDGE_IFACE;
use crate::net::network_interface::device::DeviceProxy;
use crate::net::gateway::device::DeviceProxy;
use crate::net::utils::ipv6_is_link_local;
use crate::net::web_server::Accept;
use crate::prelude::*;
@@ -43,7 +43,7 @@ use crate::util::serde::{display_serializable, HandlerExtSerde};
use crate::util::sync::{SyncMutex, Watch};
use crate::util::Invoke;
pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
pub fn gateway_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"list",
@@ -88,7 +88,7 @@ pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
Ok(())
})
.with_about("Show network interfaces StartOS can listen on")
.with_about("Show gateways StartOS can listen on")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -96,26 +96,26 @@ pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
from_fn_async(set_public)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Indicate whether this interface has inbound access from the WAN")
.with_about("Indicate whether this gateway has inbound access from the WAN")
.with_call_remote::<CliContext>(),
).subcommand(
"unset-inbound",
"unset-public",
from_fn_async(unset_public)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Allow this interface to infer whether it has inbound access from the WAN based on its IPv4 address")
.with_about("Allow this gateway to infer whether it has inbound access from the WAN based on its IPv4 address")
.with_call_remote::<CliContext>(),
).subcommand("forget",
from_fn_async(forget_iface)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Forget a disconnected interface")
.with_about("Forget a disconnected gateway")
.with_call_remote::<CliContext>()
).subcommand("set-name",
from_fn_async(set_name)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Rename an interface")
.with_about("Rename a gateway")
.with_call_remote::<CliContext>()
)
}
@@ -814,7 +814,7 @@ impl NetworkInterfaceController {
db.as_public_mut()
.as_server_info_mut()
.as_network_mut()
.as_network_interfaces_mut()
.as_gateways_mut()
.ser(info)
})
.await
@@ -881,7 +881,7 @@ impl NetworkInterfaceController {
.as_public()
.as_server_info()
.as_network()
.as_network_interfaces()
.as_gateways()
.de()
{
Ok(mut info) => {
@@ -940,7 +940,7 @@ impl NetworkInterfaceController {
let mut sub = self
.db
.subscribe(
"/public/serverInfo/network/networkInterfaces"
"/public/serverInfo/network/gateways"
.parse::<JsonPointer<_, _>>()
.with_kind(ErrorKind::Database)?,
)
@@ -973,7 +973,7 @@ impl NetworkInterfaceController {
let mut sub = self
.db
.subscribe(
"/public/serverInfo/network/networkInterfaces"
"/public/serverInfo/network/gateways"
.parse::<JsonPointer<_, _>>()
.with_kind(ErrorKind::Database)?,
)
@@ -1043,7 +1043,7 @@ impl NetworkInterfaceController {
let (dump, mut sub) = self
.db
.dump_and_sub(
"/public/serverInfo/network/networkInterfaces"
"/public/serverInfo/network/gateways"
.parse::<JsonPointer<_, _>>()
.with_kind(ErrorKind::Database)?
.join_end(interface.as_str())

View File

@@ -34,6 +34,8 @@ pub enum HostAddress {
#[derive(Debug, Deserialize, Serialize, TS)]
pub struct DomainConfig {
#[ts(type = "string")]
pub root: InternedString,
pub public: bool,
pub acme: Option<AcmeProvider>,
}
@@ -177,7 +179,7 @@ pub struct AddDomainParams {
pub async fn add_domain<Kind: HostApiKind>(
ctx: RpcContext,
AddDomainParams {
domain,
ref domain,
private,
acme,
}: AddDomainParams,
@@ -185,24 +187,41 @@ pub async fn add_domain<Kind: HostApiKind>(
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
let root = db
.as_public()
.as_server_info()
.as_network()
.as_domains()
.keys()?
.into_iter()
.find(|root| domain.ends_with(&**root))
.or_not_found(lazy_format!("root domain for {domain}"))?;
if let Some(acme) = &acme {
if !db.as_public().as_server_info().as_network().as_acme().contains_key(&acme)? {
if !db
.as_public()
.as_server_info()
.as_network()
.as_acme()
.contains_key(&acme)?
{
return Err(Error::new(eyre!("unknown acme provider {}, please run acme.init for this provider first", acme.0), ErrorKind::InvalidRequest));
}
}
Kind::host_for(&inheritance, db)?
.as_domains_mut()
.insert(
&domain,
&DomainConfig {
public: !private,
acme,
},
)?;
Kind::host_for(&inheritance, db)?.as_domains_mut().insert(
domain,
&DomainConfig {
root,
public: !private,
acme,
},
)?;
check_duplicates(db)
})
.await.result?;
.await
.result?;
Kind::sync_host(&ctx, inheritance).await?;
Ok(())

View File

@@ -13,7 +13,7 @@ use crate::context::{CliContext, RpcContext};
use crate::db::model::public::NetworkInterfaceInfo;
use crate::net::forward::AvailablePorts;
use crate::net::host::HostApiKind;
use crate::net::network_interface::InterfaceFilter;
use crate::net::gateway::InterfaceFilter;
use crate::net::vhost::AlpnInfo;
use crate::prelude::*;
use crate::util::serde::{display_serializable, HandlerExtSerde};

View File

@@ -53,7 +53,7 @@ impl Host {
self.domains
.iter()
.map(
|(address, DomainConfig { public, acme })| HostAddress::Domain {
|(address, DomainConfig { public, acme, .. })| HostAddress::Domain {
address: address.clone(),
public: *public,
acme: acme.clone(),

View File

@@ -2,12 +2,13 @@ use rpc_toolkit::{Context, HandlerExt, ParentHandler};
pub mod acme;
pub mod dns;
pub mod domain;
pub mod forward;
pub mod gateway;
pub mod host;
pub mod keys;
pub mod mdns;
pub mod net_controller;
pub mod network_interface;
pub mod service_interface;
pub mod ssl;
pub mod static_server;
@@ -18,20 +19,23 @@ pub mod vhost;
pub mod web_server;
pub mod wifi;
pub fn net<C: Context>() -> ParentHandler<C> {
pub fn net_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"tor",
tor::tor::<C>().with_about("Tor commands such as list-services, logs, and reset"),
tor::tor_api::<C>().with_about("Tor commands such as list-services, logs, and reset"),
)
.subcommand(
"acme",
acme::acme::<C>().with_about("Setup automatic clearnet certificate acquisition"),
acme::acme_api::<C>().with_about("Setup automatic clearnet certificate acquisition"),
)
.subcommand(
"network-interface",
network_interface::network_interface_api::<C>()
.with_about("View and edit network interface configurations"),
"domain",
domain::domain_api::<C>().with_about("Setup clearnet domains"),
)
.subcommand(
"gateway",
gateway::gateway_api::<C>().with_about("View and edit gateway configurations"),
)
.subcommand(
"tunnel",

View File

@@ -17,13 +17,13 @@ use crate::error::ErrorCollection;
use crate::hostname::Hostname;
use crate::net::dns::DnsController;
use crate::net::forward::{PortForwardController, START9_BRIDGE_IFACE};
use crate::net::host::address::HostAddress;
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
use crate::net::host::{host_for, Host, Hosts};
use crate::net::network_interface::{
use crate::net::gateway::{
AndFilter, DynInterfaceFilter, InterfaceFilter, LoopbackFilter, NetworkInterfaceController,
SecureFilter,
};
use crate::net::host::address::HostAddress;
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
use crate::net::host::{host_for, Host, Hosts};
use crate::net::service_interface::{HostnameInfo, IpHostname, OnionHostname};
use crate::net::tor::TorController;
use crate::net::utils::ipv6_is_local;

View File

@@ -84,7 +84,7 @@ lazy_static! {
static ref PROGRESS_REGEX: Regex = Regex::new("PROGRESS=([0-9]+)").unwrap();
}
pub fn tor<C: Context>() -> ParentHandler<C> {
pub fn tor_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"list-services",

View File

@@ -53,7 +53,7 @@ pub async fn add_tunnel(
.into_public()
.into_server_info()
.into_network()
.into_network_interfaces()
.into_gateways()
.keys()?;
let mut iface = GatewayId::from("wg0");
for id in 1.. {
@@ -105,7 +105,7 @@ pub async fn remove_tunnel(
.into_public()
.into_server_info()
.into_network()
.into_network_interfaces()
.into_gateways()
.into_idx(&id)
.and_then(|e| e.into_ip_info().transpose())
else {

View File

@@ -36,7 +36,7 @@ use crate::context::{CliContext, RpcContext};
use crate::db::model::public::NetworkInterfaceInfo;
use crate::db::model::Database;
use crate::net::acme::{AcmeCertCache, AcmeProvider};
use crate::net::network_interface::{
use crate::net::gateway::{
Accepted, AnyFilter, DynInterfaceFilter, InterfaceFilter, NetworkInterfaceController,
NetworkInterfaceListener,
};

View File

@@ -14,7 +14,7 @@ use tokio::net::{TcpListener, TcpStream};
use tokio::sync::oneshot;
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::net::network_interface::{
use crate::net::gateway::{
lookup_info_by_addr, NetworkInterfaceListener, SelfContainedNetworkInterfaceListener,
};
use crate::net::static_server::{

View File

@@ -90,7 +90,7 @@ pub async fn get_ssl_certificate(
.as_public()
.as_server_info()
.as_network()
.as_network_interfaces()
.as_gateways()
.as_entries()?
.into_iter()
.flat_map(|(_, net)| net.as_ip_info().transpose_ref())

View File

@@ -20,7 +20,7 @@ use crate::context::CliContext;
use crate::middleware::auth::AuthContext;
use crate::middleware::signature::SignatureAuthContext;
use crate::net::forward::PortForwardController;
use crate::net::network_interface::NetworkInterfaceWatcher;
use crate::net::gateway::NetworkInterfaceWatcher;
use crate::prelude::*;
use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations};
use crate::tunnel::db::TunnelDatabase;

View File

@@ -49,7 +49,9 @@ mod v0_4_0_alpha_7;
mod v0_4_0_alpha_8;
mod v0_4_0_alpha_9;
pub type Current = v0_4_0_alpha_9::Version; // VERSION_BUMP
mod v0_4_0_alpha_10;
pub type Current = v0_4_0_alpha_10::Version; // VERSION_BUMP
impl Current {
#[instrument(skip(self, db))]
@@ -161,7 +163,8 @@ enum Version {
V0_4_0_alpha_6(Wrapper<v0_4_0_alpha_6::Version>),
V0_4_0_alpha_7(Wrapper<v0_4_0_alpha_7::Version>),
V0_4_0_alpha_8(Wrapper<v0_4_0_alpha_8::Version>),
V0_4_0_alpha_9(Wrapper<v0_4_0_alpha_9::Version>), // VERSION_BUMP
V0_4_0_alpha_9(Wrapper<v0_4_0_alpha_9::Version>),
V0_4_0_alpha_10(Wrapper<v0_4_0_alpha_10::Version>), // VERSION_BUMP
Other(exver::Version),
}
@@ -213,7 +216,8 @@ impl Version {
Self::V0_4_0_alpha_6(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_7(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_8(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_9(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::V0_4_0_alpha_9(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_10(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::Other(v) => {
return Err(Error::new(
eyre!("unknown version {v}"),
@@ -257,7 +261,8 @@ impl Version {
Version::V0_4_0_alpha_6(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_7(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_8(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_9(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::V0_4_0_alpha_9(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_10(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::Other(x) => x.clone(),
}
}

View File

@@ -72,8 +72,9 @@ impl VersionT for Version {
}
HostAddress::Domain { address } => {
domains.insert(
address,
address.clone(),
DomainConfig {
root: address,
public: true,
acme: None,
},

View File

@@ -0,0 +1,108 @@
use std::collections::BTreeSet;
use std::sync::Arc;
use exver::{PreReleaseSegment, VersionRange};
use imbl_value::json;
use super::v0_3_5::V0_3_0_COMPAT;
use super::{v0_4_0_alpha_9, VersionT};
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_10: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 10.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_9::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_10.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument]
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
let default_gateway = db["public"]["serverInfo"]["network"]["networkInterfaces"]
.as_object()
.into_iter()
.flatten()
.find(|(_, i)| i["ipInfo"]["wanIp"].is_string())
.map(|(g, _)| g.clone());
let mut roots = BTreeSet::new();
for (_, package) in db["public"]["packageData"]
.as_object_mut()
.ok_or_else(|| {
Error::new(
eyre!("expected public.packageData to be an object"),
ErrorKind::Database,
)
})?
.iter_mut()
{
for (_, host) in package["hosts"]
.as_object_mut()
.ok_or_else(|| {
Error::new(
eyre!("expected public.packageData[id].hosts to be an object"),
ErrorKind::Database,
)
})?
.iter_mut()
{
if default_gateway.is_none() {
host["domains"] = json!({});
continue;
}
for (domain, info) in host["domains"]
.as_object_mut()
.ok_or_else(|| {
Error::new(
eyre!(
"expected public.packageData[id].hosts[id].domains to be an object"
),
ErrorKind::Database,
)
})?
.iter_mut()
{
let Some(info) = info.as_object_mut() else {
continue;
};
let root = domain.clone();
info.insert("root".into(), Value::String(Arc::new((&*root).to_owned())));
roots.insert(root);
}
}
}
let network = db["public"]["serverInfo"]["network"]
.as_object_mut()
.ok_or_else(|| {
Error::new(
eyre!("expected public.serverInfo.network to be an object"),
ErrorKind::Database,
)
})?;
network["gateways"] = network["networkInterfaces"].clone();
if let Some(gateway) = default_gateway {
for root in roots {
network["domains"][&*root] = json!({ "gateway": gateway });
}
}
Ok(Value::Null)
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}