{ config, lib, pkgs, options, ... }: with lib; let cfg = config.services.ipfs-cluster; opt = options.services.ipfs-cluster; # secret is by envvar, not flag initFlags = toString [ (optionalString (cfg.initPeers != [ ]) "--peers") (lib.strings.concatStringsSep "," cfg.initPeers) ]; serviceJson = builtins.toFile "ipfs-cluster-service.json" (builtins.toJSON cfg.settings); pinSvcBasicAuthFile = if cfg.pinSvcBasicAuthFile != null then cfg.pinSvcBasicAuthFile else "/dev/null"; in { ###### interface options = { services.ipfs-cluster = { enable = mkEnableOption "Pinset orchestration for IPFS - requires ipfs daemon to be useful"; package = mkOption { type = types.package; default = pkgs.ipfs-cluster; description = "ipfs-cluster package"; }; user = mkOption { type = types.str; default = "ipfs"; description = "User under which the ipfs-cluster daemon runs."; }; group = mkOption { type = types.str; default = "ipfs"; description = "Group under which the ipfs-cluster daemon runs."; }; consensus = mkOption { type = types.enum [ "raft" "crdt" ]; description = "Consensus protocol - 'raft' or 'crdt'. https://cluster.ipfs.io/documentation/guides/consensus/"; }; dataDir = mkOption { type = types.str; default = "/var/lib/ipfs-cluster"; description = "The data dir for ipfs-cluster."; }; initPeers = mkOption { type = types.listOf types.str; default = [ ]; description = "Peer addresses to initialize with on first run."; }; openSwarmPort = mkOption { type = types.bool; description = "Open swarm port, secured by the cluster secret. This does not expose the API or proxy. https://cluster.ipfs.io/documentation/guides/security/"; }; secretFile = mkOption { type = types.nullOr types.path; default = null; description = '' File containing the cluster secret in the format of EnvironmentFile as described by systemd.exec 5. For example: CLUSTER_SECRET=... if null, a new secret will be generated on first run. A secret in the correct format can also be generated by: openssl rand -hex 32 ''; }; settings = mkOption { type = types.attrs; default = {}; description = "Configuration to be merged into service.json"; }; pinSvcBasicAuthFile = mkOption { type = types.nullOr types.path; default = null; description = '' JSON file containing basic auth credentials for the pinning service API. Use the following format: { "username1": "password1", "username2": "password2" } ''; }; }; }; ###### implementation config = mkIf cfg.enable { environment.systemPackages = [ cfg.package ]; systemd.tmpfiles.rules = [ "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -" ]; systemd.services.ipfs-cluster-init = { path = [ "/run/wrappers" cfg.package ]; environment.IPFS_CLUSTER_PATH = cfg.dataDir; wantedBy = [ "default.target" ]; serviceConfig = { # "" clears exec list (man systemd.service -> execStart) ExecStart = [ "" "${cfg.package}/bin/ipfs-cluster-service init --consensus ${cfg.consensus} ${initFlags}" ]; Type = "oneshot"; RemainAfterExit = true; User = cfg.user; Group = cfg.group; } // optionalAttrs (cfg.secretFile != null) { EnvironmentFile = cfg.secretFile; }; unitConfig.ConditionDirectoryNotEmpty = "!${cfg.dataDir}"; }; systemd.services.ipfs-cluster = { environment.IPFS_CLUSTER_PATH = cfg.dataDir; wantedBy = [ "multi-user.target" ]; wants = [ "ipfs-cluster-init.service" ]; after = [ "ipfs-cluster-init.service" ]; preStart = '' ${pkgs.jq}/bin/jq --slurp '.[0] * .[1] * { api: { pinsvcapi: { basic_auth_credentials: .[2] } } }' \ '${cfg.dataDir}/service.json' ${serviceJson} '${pinSvcBasicAuthFile}' \ | install -D -m 600 -o '${cfg.user}' -g '${cfg.group}' /dev/stdin '${cfg.dataDir}/.service.json.new' mv '${cfg.dataDir}/.service.json.new' '${cfg.dataDir}/service.json' ''; serviceConfig = { Type = "notify"; RuntimeDirectory = "ipfs-cluster"; ExecStart = [ "" "${cfg.package}/bin/ipfs-cluster-service daemon" ]; User = cfg.user; Group = cfg.group; }; }; networking.firewall.allowedTCPPorts = mkIf cfg.openSwarmPort [ 9096 ]; }; }