Aller au contenu

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.

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";
};

Toujours lib.mkMerge de deux blocs :

  1. Bloc inconditionnel — enregistrement auprès du système de services et templates (proxy, persist, OAuth2…). Toujours évalué, même service désactivé.
  2. 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 :

Diagram

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 importer dnf/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.

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 sur params.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 depuis config.yaml. L’icon utilise 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.

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 :

DrapeauDéfautRôle
reverseProxytrueExpose le service via Caddy
uniquePerZonefalseUn seul exemplaire par zone
externalAccessfalseAccè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: true

Le framework résout domain/global en params (fqdn, href, ip…) via dnfLib.extractServiceParams host network "oxicloud" defaultParams.

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.

  1. Nommer la clé <service>[Usage] — le préfixe ports. dit déjà « port », la clé précise donc à quoi il sert (kanidmReplication, garageRpc). Un <service> seul désigne le port principal. Garder network.nix trié par valeur.

  2. 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;
  3. Port hérité d’un upstream (config.services.<x>.port, non lié par DNF) : l’inscrire plutôt dans ports.reserved (denylist anti-conflit) ; le module continue de lire config.services.<x>.port.

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 ];

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; # sauvegarde

Persister 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).

  • Déclarer sops.secrets.<nom> = { mode = "0400"; owner = "<svc-user>"; } (fichier par défaut usr/secrets/secrets.yaml).
  • Un service DynamicUser = true ne peut pas posséder un secret sops. S’il charge la clé via LoadCredential (lu en root), un secret root 0400 suffit ; sinon, passer par un groupe statique + SupplementaryGroups.

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 :

  1. Template OAuth2 (bloc inconditionnel) : darkone.service.idm.oauth2.<nom>. Kanidm le provisionne en client OAuth2 quand idm est activé.
  2. Contexte OIDC : dnfLib.mkOidcContextclientId, secret, idmUrl. idmUrl == null quand aucun idm n’existe : on coupe alors tout le câblage.
  3. Endpoints : dnfLib.mkKanidmEndpoints idmUrl clientIdissuerUrl, 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 ];
};

Quand seul le module NixOS manque (paquet déjà dans nixpkgs), on le source depuis un fork portant la PR, sans le reconstruire.

  1. Ajouter dans dnf/flake.nix un input dédié pointant la branche du fork, sans inputs.nixpkgs.follows (on veut l’arbre du fork tel quel) :

    nixpkgs-oxicloud.url = "github:flashonfire/nixpkgs/oxicloud-service";
  2. Injecter le path du module dans la modules list de mkNode (dnf/lib/mk-configuration.nix) — parsé inconditionnellement, sans effet tant que services.<nom>.enable = false :

    "${nixpkgs-oxicloud}/nixos/modules/services/web-apps/oxicloud.nix"
  1. just generate — régénère les default.nix (auto-import du nouveau module).
  2. just clean — fix + check + generate + format (obligatoire avant commit).
  3. É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.