diff --git a/README.md b/README.md index 0494802..a822603 100644 --- a/README.md +++ b/README.md @@ -303,36 +303,18 @@ You can start a lookup-server service when starting an instance: haze start --name gs-master lookup ``` -The lookup server is then accessible by other instances at -`http://haze-gs-master-lookup`. - The Nextcloud instance started with this command is already properly configured as master node. -For the slaves instance, here is a sane preset: - -```toml -[[preset]] -name = "gs-slave" -apps = ["globalsiteselector"] -config = { - "gs.enabled" = true, - "gs.federation" = "global", - "gss.jwt.key" = "random-key", # Matches the key set in the docker container of the lookup server. - "gss.mode" = "slave", -} -``` - -You'll then have to manually point your slave instances to the lookup and master -instances: +You can then start slave instance with the following command: ```bash -haze start --name gs-slave1 gs-slave -# Assuming that the master instance was started with `--name gs-master`. -haze gs-slave1 occ config:system:set gss.master.url --value="http://haze-gs-master.haze.example.com" -haze gs-slave1 occ config:system:set lookup_server --value="http://haze-gs-master-lookup" +haze start --name gs-slave1 slave ``` +A slave attaches to the most recently started instance running a lookup server, +and is pointed at both that lookup server and the master instance automatically. + If you want to use the `ManualUserMapping` module, you'll have to set the following config on the master instance: diff --git a/src/service.rs b/src/service.rs index bce3e1e..8afd4b8 100644 --- a/src/service.rs +++ b/src/service.rs @@ -15,6 +15,7 @@ mod push; mod redis; mod sftp; mod sharded; +mod slave; mod smb; mod webhook; @@ -37,6 +38,7 @@ pub use crate::service::push::NotifyPush; use crate::service::redis::Redis; use crate::service::sftp::{Sftp, SftpKey}; use crate::service::sharded::{Sharding, ShardingMigrate, ShardingMigrateUnset, SingleShard}; +pub use crate::service::slave::GlobalScaleSlave; use crate::service::smb::Smb; use crate::service::webhook::Webhook; use bollard::Docker; @@ -337,6 +339,9 @@ pub enum ServiceType { /// Global scale lookup server #[strum(serialize = "lookup")] LookupServer, + /// Global scale slave instance + #[strum(serialize = "slave")] + GlobalScaleSlave, } #[enum_dispatch] @@ -375,6 +380,7 @@ pub enum Service { AuthentikOidc(AuthentikOidc), AuthentikScim(AuthentikScim), LookupServer(LookupServer), + GlobalScaleSlave(GlobalScaleSlave), Preset(PresetService), } @@ -435,6 +441,9 @@ impl Service { Service::AuthentikScim(AuthentikScim), ]), ServiceType::LookupServer => Some(vec![Service::LookupServer(LookupServer)]), + ServiceType::GlobalScaleSlave => { + Some(vec![Service::GlobalScaleSlave(GlobalScaleSlave)]) + } } } else { presets diff --git a/src/service/lookup_server.rs b/src/service/lookup_server.rs index 5562af8..8e6099f 100644 --- a/src/service/lookup_server.rs +++ b/src/service/lookup_server.rs @@ -15,6 +15,8 @@ use serde_json::Value; use std::collections::HashMap; use std::io::Stdout; +pub(super) const GSS_JWT_KEY: &str = "random-key"; + #[derive(Debug, Clone, Eq, PartialEq)] pub struct LookupServer; @@ -136,7 +138,7 @@ impl ServiceTrait for LookupServer { "gss.mode".into() => Value::String("master".into()), "gss.master.accounts".into() => Value::Array(vec![Value::String("admin".into())]), "gss.master.csp-allow".into() => Value::Array(vec![Value::String("*".into())]), - "gss.jwt.key".into() => Value::String("random-key".into()), + "gss.jwt.key".into() => Value::String(GSS_JWT_KEY.into()), }) } } diff --git a/src/service/slave.rs b/src/service/slave.rs new file mode 100644 index 0000000..023f0ea --- /dev/null +++ b/src/service/slave.rs @@ -0,0 +1,130 @@ +use crate::Result; +use crate::cloud::{Cloud, CloudOptions}; +use crate::config::{HazeConfig, ProxyConfig}; +use crate::service::lookup_server::GSS_JWT_KEY; +use crate::service::{LookupServer, ServiceTrait}; +use bollard::Docker; +use bollard::query_parameters::ListContainersOptions; +use maplit::hashmap; +use miette::{IntoDiagnostic, Report}; +use serde_json::Value; +use std::collections::HashMap; + +/// Cloud id of the most recently started lookup server. +async fn master_cloud_id(docker: &Docker) -> Result { + let mut lookup_servers: Vec<_> = docker + .list_containers(Some(ListContainersOptions { + all: true, + ..Default::default() + })) + .await + .into_diagnostic()? + .iter() + .filter_map(|container| { + let labels = container.labels.as_ref()?; + if labels.get("haze-type").map(String::as_str) != Some(LookupServer.name()) { + return None; + } + let cloud_id = labels.get("haze-cloud-id")?.clone(); + Some((container.created.unwrap_or_default(), cloud_id)) + }) + .collect(); + + lookup_servers.sort_by(|a, b| a.0.cmp(&b.0).reverse()); + + lookup_servers.into_iter().map(|(_created, id)| id).next().ok_or_else(|| { + Report::msg("No haze instance with a lookup server is running, start one with `haze start lookup`") + }) +} + +/// Latest cloud started with a lookup server. +async fn master_instance(docker: &Docker, config: &HazeConfig) -> Result { + let id = master_cloud_id(docker).await?; + Cloud::list(docker, Some(id.clone()), config) + .await? + .into_iter() + .find(|cloud| cloud.id == id) + .ok_or_else(|| Report::msg(format!("Failed to get the address of master instance {id}"))) +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct GlobalScaleSlave; + +#[async_trait::async_trait] +impl ServiceTrait for GlobalScaleSlave { + fn name(&self) -> &str { + "slave" + } + + async fn spawn( + &self, + docker: &Docker, + _cloud_id: &str, + _network: &str, + config: &HazeConfig, + _options: &CloudOptions, + ) -> Result> { + master_instance(docker, config).await?; + + Ok(Vec::new()) + } + + fn apps(&self) -> &'static [&'static str] { + &["globalsiteselector"] + } + + fn config( + &self, + _docker: &Docker, + _cloud_id: &str, + _config: &HazeConfig, + ) -> Result> { + Ok(hashmap! { + "gs.enabled".into() => Value::Bool(true), + "gss.mode".into() => Value::String("slave".into()), + "gss.jwt.key".into() => Value::String(GSS_JWT_KEY.into()), + }) + } + + async fn post_setup( + &self, + docker: &Docker, + _cloud_id: &str, + config: &HazeConfig, + ) -> Result>> { + let master = master_instance(docker, config).await?; + let lookup_server = LookupServer + .container_name(&master.id) + .expect("lookup server has a container name"); + + Ok(vec![ + vec![ + "occ".into(), + "config:system:set".into(), + "--silent".into(), + "lookup_server".into(), + "--value".into(), + format!("http://{lookup_server}"), + ], + vec![ + "occ".into(), + "config:system:set".into(), + "--silent".into(), + "gss.master.url".into(), + "--value".into(), + master.address.clone(), + ], + vec!["occ".into(), "globalsiteselector:users:update".into()], + ]) + } + + async fn start_message( + &self, + docker: &Docker, + _cloud_id: &str, + _proxy: &ProxyConfig, + ) -> Result> { + let master = master_cloud_id(docker).await?; + Ok(Some(format!("Nextcloud was setup as slave of {master}."))) + } +}