diff --git a/modules/default.nix b/modules/default.nix
index 58f65b9..7556bcd 100644
--- a/modules/default.nix
+++ b/modules/default.nix
@@ -9,6 +9,7 @@ let
hercules-ci-agent = import ./hercules-ci-agent;
hydra = import ./hydra;
hyprspace = import ./hyprspace;
+ ipfs-cluster = import ./ipfs-cluster;
maintenance = import ./maintenance;
minimal = import ./minimal;
motd = import ./motd;
diff --git a/modules/ipfs-cluster/default.nix b/modules/ipfs-cluster/default.nix
new file mode 100644
index 0000000..a6c12a2
--- /dev/null
+++ b/modules/ipfs-cluster/default.nix
@@ -0,0 +1,121 @@
+{ 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)
+ ];
+in {
+
+ ###### interface
+
+ options = {
+
+ services.ipfs-cluster = {
+
+ enable = mkEnableOption
+ "Pinset orchestration for IPFS - requires ipfs daemon to be useful";
+
+ 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
+ '';
+ };
+ };
+ };
+
+ ###### implementation
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = [ pkgs.ipfs-cluster ];
+
+
+ systemd.tmpfiles.rules =
+ [ "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -" ];
+
+ systemd.services.ipfs-cluster-init = {
+ path = [ "/run/wrappers" pkgs.ipfs-cluster ];
+ environment.IPFS_CLUSTER_PATH = cfg.dataDir;
+ wantedBy = [ "default.target" ];
+
+ serviceConfig = {
+ # "" clears exec list (man systemd.service -> execStart)
+ ExecStart = [
+ ""
+ "${pkgs.ipfs-cluster}/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" ];
+
+ serviceConfig = {
+ ExecStart =
+ [ "" "${pkgs.ipfs-cluster}/bin/ipfs-cluster-service daemon" ];
+ User = cfg.user;
+ Group = cfg.group;
+ };
+ };
+ networking.firewall.allowedTCPPorts = mkIf cfg.openSwarmPort [ 9096 ];
+ };
+}