Créer un module de type "service"
Un module “service” DNF câble un logiciel auto-hébergé (web UI, API,
stockage…) dans le framework : reverse proxy, page d’accueil, persistance,
pare-feu, SSO et activation par hôte. Cette page décrit la création et la
maintenance d’un tel module, de l’en-tête au commit, avec le module
oxicloud comme fil rouge.
Anatomie du module
Section intitulée « Anatomie du module »Un module vit dans dnf/modules/service/<nom>.nix. Il est auto-importé via
le default.nix généré (lancer just generate après création).
Commentaire de tête obligatoire : 1re ligne descriptive, puis paragraphes et
admonitions Starlight (note, tip, caution, danger) orientées usage /
maintenance.
# OxiCloud — Fast Sovereign Cloud (file storage, WebDAV, CalDAV & CardDAV).## :::note[Service currently being validated]# DNF wrapper around the nixpkgs module (from PR #516113).# :::Schéma imposé : darkone.service.<nom>.<option>. Démarrer avec la seule option
enable ; n’ajouter des options que pour ce qui n’est pas résolu par le
framework.
options = { darkone.service.oxicloud.enable = lib.mkEnableOption "Enable local OxiCloud service";};Structure de config
Section intitulée « Structure de config »Toujours lib.mkMerge de deux blocs :
- Bloc inconditionnel — enregistrement auprès du système de services et templates (proxy, persist, OAuth2…). Toujours évalué, même service désactivé.
- Bloc
lib.mkIf cfg.enable— la configuration NixOS réelle (le service upstream, le pare-feu, les secrets…).
config = lib.mkMerge [ { darkone.system.services.service.oxicloud = { /* registration */ }; darkone.service.idm.oauth2.oxicloud = { /* template OAuth2 */ }; } (lib.mkIf cfg.enable { darkone.system.services = dnfLib.enableBlock "oxicloud"; services.oxicloud = { /* … */ }; })];Structure à deux blocs :
Arguments injectés et frontière consumer
Section intitulée « Arguments injectés et frontière consumer »Outre lib, config, pkgs, chaque module de service reçoit des specialArgs :
host— hôte courant (hostname,zone,ip,vpnIp,services,profile…).hosts— tous les hôtes du déploiement (toutes zones).zone— zone de l’hôte courant (name,domain,gateway,lang…).network— config globale (domain,coordination,services,zones).dnfLib— helpers du framework (ne jamais importerdnf/lib/à la main).dnfConfig— registres de config du framework (dnfConfig.network.ports.<clé>: registre des ports).workDir— racine du workspace consumer.users— tous les utilisateurs.
Enregistrement et activation
Section intitulée « Enregistrement et activation »Exposition du service
Section intitulée « Exposition du service »Le bloc inconditionnel déclare darkone.system.services.service.<nom>, lu
par modules/system/services.nix pour générer reverse proxy, page d’accueil,
persistance et sauvegarde.
darkone.system.services.service.oxicloud = { inherit defaultParams; # title / description / icon homepage persist = { dirs = [ oxCfg.dataDir ]; # données du service dbDirs = [ config.services.postgresql.dataDir ]; }; proxy.servicePort = srvPort; # Caddy proxifie vers ce port};proxy.servicePort— port HTTP interne ; le reverse proxy Caddy s’y connecte surparams.ip. Sans reverse proxy :proxy.servicePort = null+proxy.enable = false.persist.{dirs,dbDirs,varDirs,mediaDirs,…}— chemins persistés / sauvegardés.defaultParams— métadonnées par défaut (title,description,icon), surchargeables depuisconfig.yaml. L’iconutilise le slug selfh.st 🡕 (préfixésh-par le framework).
defaultParams = { title = "OxiCloud"; description = "Fast Sovereign Cloud"; icon = "oxicloud";};Les protocoles servis sur le même port HTTP (WebDAV, CalDAV, CardDAV pour OxiCloud) passent naturellement par l’unique vhost Caddy : rien de spécial à configurer.
Activation par hôte
Section intitulée « Activation par hôte »Un service s’active sur un hôte quand il figure sous services: dans
etc/config.yaml, ou via un déclencheur de dnf/config/modules.nix :
oxicloud = { # reverseProxy = true par défaut (service web avec UI/API). activation.profiles.minimal.triggers.keys.oxicloud = [ "enable" ];};Drapeaux disponibles :
| Drapeau | Défaut | Rôle |
|---|---|---|
reverseProxy | true | Expose le service via Caddy |
uniquePerZone | false | Un seul exemplaire par zone |
externalAccess | false | Accès depuis l’extérieur |
Côté config.yaml, l’exemple
suivant installe un OxiCloud complet sur https://cloud.mondomaine.tld :
hosts: - hostname: "mon-serveur" services: oxicloud: domain: "cloud" global: trueLe framework résout domain/global en params (fqdn, href, ip…) via
dnfLib.extractServiceParams host network "oxicloud" defaultParams.
Réseau, base de données et secrets
Section intitulée « Réseau, base de données et secrets »Ports internes
Section intitulée « Ports internes »Tout port interne qu’un module lie, ouvre au pare-feu ou proxifie
doit provenir du registre central dnf/config/network.nix, lu via
dnfConfig.network.ports.<clé> — jamais un littéral codé en dur dans le module.
C’est la source unique : un même numéro n’est jamais écrit deux fois et deux
services ne peuvent pas entrer silencieusement en conflit.
-
Nommer la clé
<service>[Usage]— le préfixeports.dit déjà « port », la clé précise donc à quoi il sert (kanidmReplication,garageRpc). Un<service>seul désigne le port principal. Gardernetwork.nixtrié par valeur. -
Ajouter la valeur dans
ports.<clé>quand DNF lie lui-même le port, puis la lire dans le module :srvPort = dnfConfig.network.ports.oxicloud; -
Port hérité d’un upstream (
config.services.<x>.port, non lié par DNF) : l’inscrire plutôt dansports.reserved(denylist anti-conflit) ; le module continue de lireconfig.services.<x>.port.
Pare-feu interne
Section intitulée « Pare-feu interne »Service web derrière Caddy → pas d’ouverture externe. Ouvrir le port sur les
seules interfaces internes avec dnfLib.mkInternalFirewall :
networking.firewall = dnfLib.mkInternalFirewall host zone [ srvPort ];PostgreSQL local
Section intitulée « PostgreSQL local »Beaucoup de modules upstream exposent un drapeau de création de base locale. Pour OxiCloud, l’option suffit (auth peer via socket) :
services.oxicloud.createLocalDatabase = true; # crée base + rôle « oxicloud »services.postgresqlBackup.enable = true; # sauvegardePersister le dataDir PostgreSQL via persist.dbDirs (voir plus haut). À
défaut d’option upstream, créer base et rôle à la main (cf. matrix.nix).
Secrets sops
Section intitulée « Secrets sops »- Déclarer
sops.secrets.<nom> = { mode = "0400"; owner = "<svc-user>"; }(fichier par défautusr/secrets/secrets.yaml). - Un service
DynamicUser = truene peut pas posséder un secret sops. S’il charge la clé viaLoadCredential(lu en root), un secret root0400suffit ; sinon, passer par un groupe statique +SupplementaryGroups.
Authentification OIDC avec Kanidm
Section intitulée « Authentification OIDC avec Kanidm »Le câblage SSO est automatique et conditionnel à la présence d’un service
idm (Kanidm) sur le réseau (zone ou global). Trois ingrédients :
- Template OAuth2 (bloc inconditionnel) :
darkone.service.idm.oauth2.<nom>. Kanidm le provisionne en client OAuth2 quandidmest activé. - Contexte OIDC :
dnfLib.mkOidcContext→clientId,secret,idmUrl.idmUrl == nullquand aucunidmn’existe : on coupe alors tout le câblage. - Endpoints :
dnfLib.mkKanidmEndpoints idmUrl clientId→issuerUrl,authUrl,tokenUrl,userinfoUrl…
params = dnfLib.extractServiceParams host network "oxicloud" defaultParams;inherit (dnfLib.mkOidcContext { name = "oxicloud"; inherit params network hosts;}) clientId secret idmUrl;oidc = dnfLib.mkKanidmEndpoints idmUrl clientId;hasIdm = idmUrl != null;Le template déclare des chemins de redirection (préfixés par le framework de l’URL publique du service) et une landing page :
darkone.service.idm.oauth2.oxicloud = { displayName = "OxiCloud"; imageFile = ./../../assets/app-icons/oxicloud.svg; redirectPaths = [ "/api/auth/oidc/callback" ]; landingPath = "/";};Le secret OAuth2 est généré côté Kanidm (oidc-secret-<clientId>). Pour le
fournir au service sans le stocker dans le store Nix, le ré-encrypter pour
l’utilisateur du service (champ sops key) puis l’injecter via un fichier
d’environnement (sops.templates, format CLE=valeur) :
sops.secrets."${secret}-service" = lib.mkIf hasIdm { mode = "0400"; owner = "oxicloud"; key = secret; # alias du secret maître};
sops.templates."oxicloud-oidc-env" = lib.mkIf hasIdm { content = "OXICLOUD_OIDC_CLIENT_SECRET=${config.sops.placeholder."${secret}-service"}"; mode = "0400"; owner = "oxicloud"; restartUnits = [ "oxicloud.service" ];};
services.oxicloud = { settings.oidc = lib.mkIf hasIdm { enable = true; issuerUrl = oidc.issuerUrl; inherit clientId; redirectUri = "${params.href}/api/auth/oidc/callback"; frontendUrl = params.href; }; environmentFiles = lib.mkIf hasIdm [ config.sops.templates."oxicloud-oidc-env".path ];};Sourcer un module upstream
Section intitulée « Sourcer un module upstream »Quand seul le module NixOS manque (paquet déjà dans nixpkgs), on le source depuis un fork portant la PR, sans le reconstruire.
-
Ajouter dans
dnf/flake.nixun input dédié pointant la branche du fork, sansinputs.nixpkgs.follows(on veut l’arbre du fork tel quel) :nixpkgs-oxicloud.url = "github:flashonfire/nixpkgs/oxicloud-service"; -
Injecter le path du module dans la
moduleslist demkNode(dnf/lib/mk-configuration.nix) — parsé inconditionnellement, sans effet tant queservices.<nom>.enable = false:"${nixpkgs-oxicloud}/nixos/modules/services/web-apps/oxicloud.nix"
Finaliser et committer
Section intitulée « Finaliser et committer »just generate— régénère lesdefault.nix(auto-import du nouveau module).just clean— fix + check + generate + format (obligatoire avant commit).- Évaluer / construire un hôte pour valider, puis commiter.
Message de commit : une ligne, 80 caractères max, anglais, format
<type>(<sujet>): <message> — ex. module(oxicloud): full DNF service with OIDC.