Services and roles
A service such as backup has roles: one server, many clients. The instance says which machine takes which role. This is clan’s inventory model, in plain yen.
{ yen, ... }:{ yen.host.nas.addr = "10.0.0.10"; yen.host.web.addr = "10.0.0.1"; yen.host.db.addr = "10.0.0.2"; yen.instance.backup.roles = { server = [ yen.host.nas ]; client = [ yen.host.web yen.host.db ]; };}Each machine spawns one member scope per (instance, role) it takes part in. An aspect at a member scope runs once
per instance (clan’s perInstance). An aspect at a machine that has a member runs once per machine (perMachine).
# A service instance is data: which machines take which role. Each machine spawns one `member` scope per role it has.{ yen, lib, ... }:let inherit (yen.lib) same is; membersOf = m: lib.listToAttrs ( lib.concatLists ( lib.mapAttrsToList ( n: i: lib.concatMap ( role: lib.optional (lib.any (same m) i.roles.${role}) ( lib.nameValuePair "${n}.${role}" { instance = n; inherit role; } ) ) (lib.attrNames i.roles) ) yen.instance ) );in{ yen.kinds.fleet = { spawns.host = _: yen.host; sinks = { out.reduce = yen.lib.reduce.merge; exports.reduce = yen.lib.reduce.entries; # what members publish, with the scope that published it }; }; yen.kinds.instance.fields.roles = is lib.isAttrs; # role name -> host refs yen.kinds.host = { fields.addr = lib.types.str; spawns.member = membersOf; sinks.config = { reduce = yen.lib.reduce.merge; out = { host, config, ... }: { config.${host.name} = config; }; }; }; yen.kinds.member = { }; # its elements go up to its machine
yen.fleet.main = { }; yen.flake = { out, ... }: out;}flowchart TD n6c4c773b6d["fleet main"] nbe39bbfa9b["host db"] n8252fa3265["member backup.client"] ncaf29b11ff["host nas"] n1d79234a62["member backup.server"] n0f3c4939ab["host web"] nab77973dae["member backup.client"] n6c4c773b6d --> nbe39bbfa9b nbe39bbfa9b --> n8252fa3265 n6c4c773b6d --> ncaf29b11ff ncaf29b11ff --> n1d79234a62 n6c4c773b6d --> n0f3c4939ab n0f3c4939ab --> nab77973dae
Members exchange data
Section titled “Members exchange data”Clients publish their address to a fleet sink with entries. The engine sets from, so a member can only publish
as itself. The server reads the entries and builds its allow list:
# The service: an aspect per role, at member scopes. Clients export their address; the server reads them.{ yen, lib, ... }:let role = r: yen.lib.all [ yen.kinds.member (s: s.instance == "backup" && s.role == r) ];in{ yen.aspects.backup-client = { at = role "client"; exports = { host, ... }: host.addr; # published by the member scope: the engine sets `from` config = { host, ... }: { backup.target = yen.host.nas.addr; backup.name = host.name; }; }; yen.aspects.backup-server = { at = role "server"; config = { exports, ... }: { backup.allow = lib.sort (a: b: a < b) ( map (e: e.value) (lib.filter (e: e.from.kind == "member") exports) ); }; }; # once per machine that takes part in any role of the service yen.aspects.backup-tools = { at = yen.lib.has (s: s.kind == "member" && s.instance == "backup"); config.packages = [ "borgbackup" ]; };}{ db = { backup = { name = "db"; target = "10.0.0.10"; }; packages = [ "borgbackup" ]; }; nas = { backup = { allow = [ "10.0.0.1" "10.0.0.2" ]; }; packages = [ "borgbackup" ]; }; web = { backup = { name = "web"; target = "10.0.0.10"; }; packages = [ "borgbackup" ]; };}