1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-10-01 08:44:09 +02:00

feat(services): Add slave service to configure global scale slave with

the latest lookup server

Signed-off-by: Louis Chmn <louis@chmn.me>
This commit is contained in:
Louis Chmn 2026-09-16 23:18:13 +02:00
commit d48a192f3f
4 changed files with 147 additions and 24 deletions

View file

@ -303,36 +303,18 @@ You can start a lookup-server service when starting an instance:
haze start --name gs-master lookup 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 The Nextcloud instance started with this command is already properly configured
as master node. as master node.
For the slaves instance, here is a sane preset: You can then start slave instance with the following command:
```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:
```bash ```bash
haze start --name gs-slave1 gs-slave haze start --name gs-slave1 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"
``` ```
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 If you want to use the `ManualUserMapping` module, you'll have to set the
following config on the master instance: following config on the master instance:

View file

@ -15,6 +15,7 @@ mod push;
mod redis; mod redis;
mod sftp; mod sftp;
mod sharded; mod sharded;
mod slave;
mod smb; mod smb;
mod webhook; mod webhook;
@ -37,6 +38,7 @@ pub use crate::service::push::NotifyPush;
use crate::service::redis::Redis; use crate::service::redis::Redis;
use crate::service::sftp::{Sftp, SftpKey}; use crate::service::sftp::{Sftp, SftpKey};
use crate::service::sharded::{Sharding, ShardingMigrate, ShardingMigrateUnset, SingleShard}; use crate::service::sharded::{Sharding, ShardingMigrate, ShardingMigrateUnset, SingleShard};
pub use crate::service::slave::GlobalScaleSlave;
use crate::service::smb::Smb; use crate::service::smb::Smb;
use crate::service::webhook::Webhook; use crate::service::webhook::Webhook;
use bollard::Docker; use bollard::Docker;
@ -337,6 +339,9 @@ pub enum ServiceType {
/// Global scale lookup server /// Global scale lookup server
#[strum(serialize = "lookup")] #[strum(serialize = "lookup")]
LookupServer, LookupServer,
/// Global scale slave instance
#[strum(serialize = "slave")]
GlobalScaleSlave,
} }
#[enum_dispatch] #[enum_dispatch]
@ -375,6 +380,7 @@ pub enum Service {
AuthentikOidc(AuthentikOidc), AuthentikOidc(AuthentikOidc),
AuthentikScim(AuthentikScim), AuthentikScim(AuthentikScim),
LookupServer(LookupServer), LookupServer(LookupServer),
GlobalScaleSlave(GlobalScaleSlave),
Preset(PresetService), Preset(PresetService),
} }
@ -435,6 +441,9 @@ impl Service {
Service::AuthentikScim(AuthentikScim), Service::AuthentikScim(AuthentikScim),
]), ]),
ServiceType::LookupServer => Some(vec![Service::LookupServer(LookupServer)]), ServiceType::LookupServer => Some(vec![Service::LookupServer(LookupServer)]),
ServiceType::GlobalScaleSlave => {
Some(vec![Service::GlobalScaleSlave(GlobalScaleSlave)])
}
} }
} else { } else {
presets presets

View file

@ -15,6 +15,8 @@ use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Stdout; use std::io::Stdout;
pub(super) const GSS_JWT_KEY: &str = "random-key";
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub struct LookupServer; pub struct LookupServer;
@ -136,7 +138,7 @@ impl ServiceTrait for LookupServer {
"gss.mode".into() => Value::String("master".into()), "gss.mode".into() => Value::String("master".into()),
"gss.master.accounts".into() => Value::Array(vec![Value::String("admin".into())]), "gss.master.accounts".into() => Value::Array(vec![Value::String("admin".into())]),
"gss.master.csp-allow".into() => Value::Array(vec![Value::String("*".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()),
}) })
} }
} }

130
src/service/slave.rs Normal file
View file

@ -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<String> {
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<Cloud> {
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<Vec<String>> {
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<HashMap<String, Value>> {
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<Vec<Vec<String>>> {
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<Option<String>> {
let master = master_cloud_id(docker).await?;
Ok(Some(format!("Nextcloud was setup as slave of {master}.")))
}
}