mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
feat: implement URL plugins with table/row actions and prefill support
- Add URL plugin effects (register, export_url, clear_urls) in core - Add PluginHostnameInfo, HostnameMetadata::Plugin, and plugin registration types - Implement plugin URL table in web UI with tableAction button and rowAction overflow menus - Thread urlPluginMetadata (packageId, hostId, interfaceId, internalPort) as prefill to actions - Add prefill support to PackageActionData so metadata passes through form dialogs - Add i18n translations for plugin error messages - Clean up plugin URLs on package uninstall
This commit is contained in:
@@ -1243,6 +1243,21 @@ backup.target.cifs.target-not-found-id:
|
||||
fr_FR: "ID de cible de sauvegarde %{id} non trouvé"
|
||||
pl_PL: "Nie znaleziono ID celu kopii zapasowej %{id}"
|
||||
|
||||
# service/effects/net/plugin.rs
|
||||
net.plugin.manifest-missing-plugin:
|
||||
en_US: "manifest does not declare the \"%{plugin}\" plugin"
|
||||
de_DE: "Manifest deklariert das Plugin \"%{plugin}\" nicht"
|
||||
es_ES: "el manifiesto no declara el plugin \"%{plugin}\""
|
||||
fr_FR: "le manifeste ne déclare pas le plugin \"%{plugin}\""
|
||||
pl_PL: "manifest nie deklaruje wtyczki \"%{plugin}\""
|
||||
|
||||
net.plugin.binding-not-found:
|
||||
en_US: "binding not found: %{binding}"
|
||||
de_DE: "Bindung nicht gefunden: %{binding}"
|
||||
es_ES: "enlace no encontrado: %{binding}"
|
||||
fr_FR: "liaison introuvable : %{binding}"
|
||||
pl_PL: "powiązanie nie znalezione: %{binding}"
|
||||
|
||||
# net/ssl.rs
|
||||
net.ssl.unreachable:
|
||||
en_US: "unreachable"
|
||||
|
||||
@@ -383,6 +383,8 @@ pub struct PackageDataEntry {
|
||||
pub store_exposed_dependents: Vec<JsonPointer>,
|
||||
#[ts(type = "string | null")]
|
||||
pub outbound_gateway: Option<GatewayId>,
|
||||
#[serde(default)]
|
||||
pub plugin: PackagePlugin,
|
||||
}
|
||||
impl AsRef<PackageDataEntry> for PackageDataEntry {
|
||||
fn as_ref(&self) -> &PackageDataEntry {
|
||||
@@ -390,6 +392,21 @@ impl AsRef<PackageDataEntry> for PackageDataEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct PackagePlugin {
|
||||
pub url: Option<UrlPluginRegistration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct UrlPluginRegistration {
|
||||
pub table_action: ActionId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct CurrentDependencies(pub BTreeMap<PackageId, CurrentDependencyInfo>);
|
||||
|
||||
@@ -75,7 +75,7 @@ impl DerivedAddressInfo {
|
||||
} else {
|
||||
!self
|
||||
.disabled
|
||||
.contains(&(h.host.clone(), h.port.unwrap_or_default())) // disablable addresses will always have a port
|
||||
.contains(&(h.hostname.clone(), h.port.unwrap_or_default())) // disablable addresses will always have a port
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -350,7 +350,7 @@ pub async fn set_address_enabled<Kind: HostApiKind>(
|
||||
} else {
|
||||
// Domains and private IPs: toggle via (host, port) in `disabled` set
|
||||
let port = address.port.unwrap_or(if address.ssl { 443 } else { 80 });
|
||||
let key = (address.host.clone(), port);
|
||||
let key = (address.hostname.clone(), port);
|
||||
if enabled {
|
||||
bind.addresses.disabled.remove(&key);
|
||||
} else {
|
||||
|
||||
@@ -92,6 +92,14 @@ impl Model<Host> {
|
||||
for (_, bind) in this.bindings.as_entries_mut()? {
|
||||
let net = bind.as_net().de()?;
|
||||
let opt = bind.as_options().de()?;
|
||||
// Preserve existing plugin-provided addresses across recomputation
|
||||
let plugin_addrs: BTreeSet<HostnameInfo> = bind
|
||||
.as_addresses()
|
||||
.as_available()
|
||||
.de()?
|
||||
.into_iter()
|
||||
.filter(|h| matches!(h.metadata, HostnameMetadata::Plugin { .. }))
|
||||
.collect();
|
||||
let mut available = BTreeSet::new();
|
||||
for (gid, g) in gateways {
|
||||
let Some(ip_info) = &g.ip_info else {
|
||||
@@ -117,7 +125,7 @@ impl Model<Host> {
|
||||
available.insert(HostnameInfo {
|
||||
ssl: opt.secure.map_or(false, |s| s.ssl),
|
||||
public: false,
|
||||
host: host.clone(),
|
||||
hostname: host.clone(),
|
||||
port: Some(port),
|
||||
metadata: metadata.clone(),
|
||||
});
|
||||
@@ -126,7 +134,7 @@ impl Model<Host> {
|
||||
available.insert(HostnameInfo {
|
||||
ssl: true,
|
||||
public: false,
|
||||
host: host.clone(),
|
||||
hostname: host.clone(),
|
||||
port: Some(port),
|
||||
metadata,
|
||||
});
|
||||
@@ -146,7 +154,7 @@ impl Model<Host> {
|
||||
available.insert(HostnameInfo {
|
||||
ssl: opt.secure.map_or(false, |s| s.ssl),
|
||||
public: true,
|
||||
host: host.clone(),
|
||||
hostname: host.clone(),
|
||||
port: Some(port),
|
||||
metadata: metadata.clone(),
|
||||
});
|
||||
@@ -155,7 +163,7 @@ impl Model<Host> {
|
||||
available.insert(HostnameInfo {
|
||||
ssl: true,
|
||||
public: true,
|
||||
host: host.clone(),
|
||||
hostname: host.clone(),
|
||||
port: Some(port),
|
||||
metadata,
|
||||
});
|
||||
@@ -182,7 +190,7 @@ impl Model<Host> {
|
||||
available.insert(HostnameInfo {
|
||||
ssl: opt.secure.map_or(false, |s| s.ssl),
|
||||
public: false,
|
||||
host: mdns_host.clone(),
|
||||
hostname: mdns_host.clone(),
|
||||
port: Some(port),
|
||||
metadata: HostnameMetadata::Mdns {
|
||||
gateways: mdns_gateways.clone(),
|
||||
@@ -193,7 +201,7 @@ impl Model<Host> {
|
||||
available.insert(HostnameInfo {
|
||||
ssl: true,
|
||||
public: false,
|
||||
host: mdns_host,
|
||||
hostname: mdns_host,
|
||||
port: Some(port),
|
||||
metadata: HostnameMetadata::Mdns {
|
||||
gateways: mdns_gateways,
|
||||
@@ -215,7 +223,7 @@ impl Model<Host> {
|
||||
available.insert(HostnameInfo {
|
||||
ssl: opt.secure.map_or(false, |s| s.ssl),
|
||||
public: true,
|
||||
host: domain.clone(),
|
||||
hostname: domain.clone(),
|
||||
port: Some(port),
|
||||
metadata: metadata.clone(),
|
||||
});
|
||||
@@ -232,7 +240,7 @@ impl Model<Host> {
|
||||
available.insert(HostnameInfo {
|
||||
ssl: true,
|
||||
public: true,
|
||||
host: domain,
|
||||
hostname: domain,
|
||||
port: Some(port),
|
||||
metadata,
|
||||
});
|
||||
@@ -257,7 +265,7 @@ impl Model<Host> {
|
||||
available.insert(HostnameInfo {
|
||||
ssl: opt.secure.map_or(false, |s| s.ssl),
|
||||
public: true,
|
||||
host: domain.clone(),
|
||||
hostname: domain.clone(),
|
||||
port: Some(port),
|
||||
metadata: HostnameMetadata::PrivateDomain { gateways },
|
||||
});
|
||||
@@ -274,7 +282,7 @@ impl Model<Host> {
|
||||
available.insert(HostnameInfo {
|
||||
ssl: true,
|
||||
public: true,
|
||||
host: domain,
|
||||
hostname: domain,
|
||||
port: Some(port),
|
||||
metadata: HostnameMetadata::PrivateDomain {
|
||||
gateways: domain_gateways,
|
||||
@@ -282,6 +290,7 @@ impl Model<Host> {
|
||||
});
|
||||
}
|
||||
}
|
||||
available.extend(plugin_addrs);
|
||||
bind.as_addresses_mut().as_available_mut().ser(&available)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ impl NetServiceData {
|
||||
| HostnameMetadata::PrivateDomain { .. } => {}
|
||||
_ => continue,
|
||||
}
|
||||
let domain = &addr_info.host;
|
||||
let domain = &addr_info.hostname;
|
||||
let domain_ssl_port = addr_info.port.unwrap_or(443);
|
||||
let key = (Some(domain.clone()), domain_ssl_port);
|
||||
let target = vhosts.entry(key).or_insert_with(|| ProxyTarget {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use imbl_value::{InOMap, InternedString};
|
||||
use imbl_value::InternedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{GatewayId, HostId, PackageId, ServiceInterfaceId};
|
||||
use crate::{ActionId, GatewayId, HostId, PackageId, ServiceInterfaceId};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
@@ -14,7 +14,7 @@ use crate::{GatewayId, HostId, PackageId, ServiceInterfaceId};
|
||||
pub struct HostnameInfo {
|
||||
pub ssl: bool,
|
||||
pub public: bool,
|
||||
pub host: InternedString,
|
||||
pub hostname: InternedString,
|
||||
pub port: Option<u16>,
|
||||
pub metadata: HostnameMetadata,
|
||||
}
|
||||
@@ -42,21 +42,22 @@ pub enum HostnameMetadata {
|
||||
gateway: GatewayId,
|
||||
},
|
||||
Plugin {
|
||||
package: PackageId,
|
||||
#[serde(flatten)]
|
||||
#[ts(skip)]
|
||||
extra: InOMap<InternedString, Value>,
|
||||
package_id: PackageId,
|
||||
row_actions: Vec<ActionId>,
|
||||
#[ts(type = "unknown")]
|
||||
#[serde(default)]
|
||||
info: Value,
|
||||
},
|
||||
}
|
||||
|
||||
impl HostnameInfo {
|
||||
pub fn to_socket_addr(&self) -> Option<SocketAddr> {
|
||||
let ip = self.host.parse().ok()?;
|
||||
let ip = self.hostname.parse().ok()?;
|
||||
Some(SocketAddr::new(ip, self.port?))
|
||||
}
|
||||
|
||||
pub fn to_san_hostname(&self) -> InternedString {
|
||||
self.host.clone()
|
||||
self.hostname.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,14 +71,66 @@ impl HostnameMetadata {
|
||||
Self::Ipv4 { gateway }
|
||||
| Self::Ipv6 { gateway, .. }
|
||||
| Self::PublicDomain { gateway } => Box::new(std::iter::once(gateway)),
|
||||
Self::PrivateDomain { gateways } | Self::Mdns { gateways } => {
|
||||
Box::new(gateways.iter())
|
||||
}
|
||||
Self::PrivateDomain { gateways } | Self::Mdns { gateways } => Box::new(gateways.iter()),
|
||||
Self::Plugin { .. } => Box::new(std::iter::empty()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginHostnameInfo {
|
||||
pub package_id: Option<PackageId>,
|
||||
pub host_id: HostId,
|
||||
pub internal_port: u16,
|
||||
pub ssl: bool,
|
||||
pub public: bool,
|
||||
#[ts(type = "string")]
|
||||
pub hostname: InternedString,
|
||||
pub port: Option<u16>,
|
||||
#[ts(type = "unknown")]
|
||||
#[serde(default)]
|
||||
pub info: Value,
|
||||
}
|
||||
|
||||
impl PluginHostnameInfo {
|
||||
/// Convert to a `HostnameInfo` with `Plugin` metadata, using the given plugin package ID.
|
||||
pub fn to_hostname_info(
|
||||
&self,
|
||||
plugin_package: &PackageId,
|
||||
row_actions: Vec<ActionId>,
|
||||
) -> HostnameInfo {
|
||||
HostnameInfo {
|
||||
ssl: self.ssl,
|
||||
public: self.public,
|
||||
hostname: self.hostname.clone(),
|
||||
port: self.port,
|
||||
metadata: HostnameMetadata::Plugin {
|
||||
package_id: plugin_package.clone(),
|
||||
info: self.info.clone(),
|
||||
row_actions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a `HostnameInfo` with Plugin metadata matches this `PluginHostnameInfo`
|
||||
/// (comparing address fields only, not row_actions).
|
||||
pub fn matches_hostname_info(&self, h: &HostnameInfo, plugin_package: &PackageId) -> bool {
|
||||
match &h.metadata {
|
||||
HostnameMetadata::Plugin { package_id, info, .. } => {
|
||||
package_id == plugin_package
|
||||
&& h.ssl == self.ssl
|
||||
&& h.public == self.public
|
||||
&& h.hostname == self.hostname
|
||||
&& h.port == self.port
|
||||
&& *info == self.info
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -602,6 +602,7 @@ fn check_matching_info_short() {
|
||||
os_version: exver::Version::new([0, 3, 6], []),
|
||||
sdk_version: None,
|
||||
hardware_acceleration: false,
|
||||
plugins: BTreeSet::new(),
|
||||
},
|
||||
icon: DataUrl::from_vec("image/png", vec![]),
|
||||
dependency_metadata: BTreeMap::new(),
|
||||
|
||||
@@ -10,6 +10,7 @@ use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
use crate::PackageId;
|
||||
use crate::service::effects::plugin::PluginId;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::asset::RegistryAsset;
|
||||
use crate::registry::context::RegistryContext;
|
||||
@@ -107,6 +108,8 @@ pub struct PackageMetadata {
|
||||
pub sdk_version: Option<Version>,
|
||||
#[serde(default)]
|
||||
pub hardware_acceleration: bool,
|
||||
#[serde(default)]
|
||||
pub plugins: BTreeSet<PluginId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||
|
||||
@@ -218,6 +218,7 @@ impl TryFrom<ManifestV1> for Manifest {
|
||||
PackageProcedure::Docker(d) => d.gpu_acceleration,
|
||||
PackageProcedure::Script(_) => false,
|
||||
},
|
||||
plugins: BTreeSet::new(),
|
||||
},
|
||||
images: BTreeMap::new(),
|
||||
volumes: value
|
||||
|
||||
@@ -14,6 +14,7 @@ mod control;
|
||||
mod dependency;
|
||||
mod health;
|
||||
mod net;
|
||||
pub mod plugin;
|
||||
mod prelude;
|
||||
pub mod subcontainer;
|
||||
mod system;
|
||||
@@ -167,6 +168,26 @@ pub fn handler<C: Context>() -> ParentHandler<C> {
|
||||
from_fn_async(net::ssl::get_ssl_certificate).no_cli(),
|
||||
)
|
||||
.subcommand("get-ssl-key", from_fn_async(net::ssl::get_ssl_key).no_cli())
|
||||
// plugin
|
||||
.subcommand(
|
||||
"plugin",
|
||||
ParentHandler::<C>::new().subcommand(
|
||||
"url",
|
||||
ParentHandler::<C>::new()
|
||||
.subcommand(
|
||||
"register",
|
||||
from_fn_async(net::plugin::register).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"export-url",
|
||||
from_fn_async(net::plugin::export_url).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"clear-urls",
|
||||
from_fn_async(net::plugin::clear_urls).no_cli(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
"set-data-version",
|
||||
from_fn_async(version::set_data_version)
|
||||
|
||||
@@ -2,4 +2,5 @@ pub mod bind;
|
||||
pub mod host;
|
||||
pub mod info;
|
||||
pub mod interface;
|
||||
pub mod plugin;
|
||||
pub mod ssl;
|
||||
|
||||
164
core/src/service/effects/net/plugin.rs
Normal file
164
core/src/service/effects/net/plugin.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::ActionId;
|
||||
use crate::net::host::{all_hosts, host_for};
|
||||
use crate::net::service_interface::{HostnameMetadata, PluginHostnameInfo};
|
||||
use crate::service::Service;
|
||||
use crate::service::effects::plugin::PluginId;
|
||||
use crate::service::effects::prelude::*;
|
||||
|
||||
fn require_url_plugin(context: &Arc<Service>) -> Result<(), Error> {
|
||||
if !context
|
||||
.seed
|
||||
.persistent_container
|
||||
.s9pk
|
||||
.as_manifest()
|
||||
.metadata
|
||||
.plugins
|
||||
.contains(&PluginId::UrlV0)
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("{}", t!("net.plugin.manifest-missing-plugin", plugin = "url-v0")),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct UrlPluginRegisterParams {
|
||||
pub table_action: ActionId,
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
context: EffectContext,
|
||||
UrlPluginRegisterParams { table_action }: UrlPluginRegisterParams,
|
||||
) -> Result<(), Error> {
|
||||
use crate::db::model::package::UrlPluginRegistration;
|
||||
|
||||
let context = context.deref()?;
|
||||
require_url_plugin(&context)?;
|
||||
let plugin_id = context.seed.id.clone();
|
||||
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&plugin_id)
|
||||
.or_not_found(&plugin_id)?
|
||||
.as_plugin_mut()
|
||||
.as_url_mut()
|
||||
.ser(&Some(UrlPluginRegistration { table_action }))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct UrlPluginExportUrlParams {
|
||||
pub hostname_info: PluginHostnameInfo,
|
||||
pub row_actions: Vec<ActionId>,
|
||||
}
|
||||
|
||||
pub async fn export_url(
|
||||
context: EffectContext,
|
||||
UrlPluginExportUrlParams {
|
||||
hostname_info,
|
||||
row_actions,
|
||||
}: UrlPluginExportUrlParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
require_url_plugin(&context)?;
|
||||
let plugin_id = context.seed.id.clone();
|
||||
|
||||
let entry = hostname_info.to_hostname_info(&plugin_id, row_actions);
|
||||
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let host = host_for(db, hostname_info.package_id.as_ref(), &hostname_info.host_id)?;
|
||||
host.as_bindings_mut()
|
||||
.as_idx_mut(&hostname_info.internal_port)
|
||||
.or_not_found(t!("net.plugin.binding-not-found", binding = format!(
|
||||
"{}:{}:{}",
|
||||
hostname_info.package_id.as_deref().unwrap_or("STARTOS"),
|
||||
hostname_info.host_id,
|
||||
hostname_info.internal_port
|
||||
)))?
|
||||
.as_addresses_mut()
|
||||
.as_available_mut()
|
||||
.mutate(|available: &mut BTreeSet<_>| {
|
||||
available.insert(entry);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct UrlPluginClearUrlsParams {
|
||||
pub except: BTreeSet<PluginHostnameInfo>,
|
||||
}
|
||||
|
||||
pub async fn clear_urls(
|
||||
context: EffectContext,
|
||||
UrlPluginClearUrlsParams { except }: UrlPluginClearUrlsParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
require_url_plugin(&context)?;
|
||||
let plugin_id = context.seed.id.clone();
|
||||
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
for host in all_hosts(db) {
|
||||
let host = host?;
|
||||
for (_, bind) in host.as_bindings_mut().as_entries_mut()? {
|
||||
bind.as_addresses_mut().as_available_mut().mutate(
|
||||
|available: &mut BTreeSet<_>| {
|
||||
available.retain(|h| {
|
||||
match &h.metadata {
|
||||
HostnameMetadata::Plugin { package_id, .. }
|
||||
if package_id == &plugin_id =>
|
||||
{
|
||||
// Keep if it matches any entry in the except list
|
||||
except
|
||||
.iter()
|
||||
.any(|e| e.matches_hostname_info(h, &plugin_id))
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
9
core/src/service/effects/plugin.rs
Normal file
9
core/src/service/effects/plugin.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[ts(export)]
|
||||
pub enum PluginId {
|
||||
UrlV0,
|
||||
}
|
||||
@@ -260,6 +260,7 @@ impl ServiceMap {
|
||||
hosts: Default::default(),
|
||||
store_exposed_dependents: Default::default(),
|
||||
outbound_gateway: None,
|
||||
plugin: Default::default(),
|
||||
},
|
||||
)?;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
use imbl::vector;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::package::{InstalledState, InstallingInfo, InstallingState, PackageState};
|
||||
use crate::net::host::all_hosts;
|
||||
use crate::net::service_interface::{HostnameInfo, HostnameMetadata};
|
||||
use crate::prelude::*;
|
||||
use crate::volume::PKG_VOLUME_DIR;
|
||||
use crate::{DATA_DIR, PACKAGE_DATA, PackageId};
|
||||
@@ -36,6 +39,24 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(),
|
||||
Ok(())
|
||||
})?;
|
||||
d.as_private_mut().as_package_stores_mut().remove(&id)?;
|
||||
// Remove plugin URLs exported by this package from all hosts
|
||||
for host in all_hosts(d) {
|
||||
let host = host?;
|
||||
for (_, bind) in host.as_bindings_mut().as_entries_mut()? {
|
||||
bind.as_addresses_mut()
|
||||
.as_available_mut()
|
||||
.mutate(|available: &mut BTreeSet<HostnameInfo>| {
|
||||
available.retain(|h| {
|
||||
!matches!(
|
||||
&h.metadata,
|
||||
HostnameMetadata::Plugin { package_id, .. }
|
||||
if package_id == id
|
||||
)
|
||||
});
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(Some(pde))
|
||||
} else {
|
||||
Ok(None)
|
||||
|
||||
Reference in New Issue
Block a user