{ config, depot, lib, pkgs, ... }: let cfg = config.services.garage; garageShellLibrary = pkgs.writeText "garage-shell-library.sh" '' getNodeId() { nodeId="" while [[ -z "$nodeId" ]]; do nodeId="$(garage status | grep 'NO ROLE ASSIGNED' | grep -wm1 "$1" | cut -d' ' -f1)" if [[ $? -ne 0 ]]; then echo "Waiting for node $1 to appear..." 2>/dev/null sleep 1 fi done echo "$nodeId" } waitForGarage() { while ! garage status >/dev/null 2>/dev/null; do sleep 1 done } waitForGarageOperational() { waitForGarage while garage layout show | grep -qwm1 '^Current cluster layout version: 0'; do sleep 1 done } ''; in { options.services.garage = with lib; { layout.initial = mkOption { default = {}; type = with types; attrsOf (submodule { options = { zone = mkOption { type = types.str; }; capacity = mkOption { type = types.ints.positive; }; }; }); }; keys = mkOption { type = with types; attrsOf (submodule { options = { allow = { createBucket = mkOption { description = "Allow the key to create new buckets."; type = bool; default = false; }; }; locksmith = { nodes = mkOption { description = "Nodes that this key will be made available to via Locksmith."; type = listOf str; default = []; }; format = mkOption { description = "Locksmith secret format."; type = enum [ "files" "aws" "envFile" "s3ql" ]; default = "files"; }; owner = mkOption { type = str; default = "root"; }; group = mkOption { type = str; default = "root"; }; mode = mkOption { type = str; default = "0400"; }; }; }; }); default = {}; }; buckets = mkOption { type = with types; attrsOf (submodule { options = { allow = mkOption { description = "List of permissions to grant on this bucket, grouped by key name."; type = attrsOf (listOf (enum [ "read" "write" "owner" ])); default = {}; }; quotas = { maxObjects = mkOption { description = "Maximum number of objects in this bucket. Null for unlimited."; type = nullOr ints.positive; default = null; }; maxSize = mkOption { description = "Maximum size of this bucket in bytes. Null for unlimited."; type = nullOr ints.positive; default = null; }; }; web.enable = mkEnableOption "website access for this bucket"; }; }); default = {}; }; }; config = { system.extraIncantations = { runGarage = i: script: i.execShellWith [ config.services.garage.package pkgs.gnugrep ] '' source ${garageShellLibrary} waitForGarage ${script} ''; }; systemd.services = { garage-layout-init = { distributed.enable = true; wantedBy = [ "garage.service" "multi-user.target" ]; wants = [ "garage.service" ]; after = [ "garage.service" ]; path = [ config.services.garage.package ]; serviceConfig = { Type = "oneshot"; TimeoutStartSec = "1800s"; Restart = "on-failure"; RestartSec = "10s"; }; script = '' source ${garageShellLibrary} waitForGarage if [[ "$(garage layout show | grep -m1 '^Current cluster layout version:' | cut -d: -f2 | tr -d ' ')" != "0" ]]; then exit 0 fi ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: layout: '' garage layout assign -z '${layout.zone}' -c '${toString layout.capacity}' "$(getNodeId '${name}')" '') cfg.layout.initial)} garage layout apply --version 1 ''; }; garage-ready = { wants = [ "garage.service" ]; after = [ "garage.service" "garage-layout-init.service" ]; path = [ config.services.garage.package ]; serviceConfig = { Type = "oneshot"; TimeoutStartSec = "1800s"; Restart = "on-failure"; RestartSec = "10s"; }; script = '' source ${garageShellLibrary} waitForGarageOperational ''; }; }; services.incandescence.providers.garage = { locksmith = true; wantedBy = [ "garage.service" "multi-user.target" ]; partOf = [ "garage.service" ]; wants = [ "garage-ready.service" ]; after = [ "garage-ready.service" ]; packages = [ config.services.garage.package ]; formulae = { key = { destroyAfterDays = 0; create = key: '' if [[ "$(garage key info ${lib.escapeShellArg key} 2>&1 >/dev/null)" == "Error: 0 matching keys" ]]; then # don't print secret key garage key new --name ${lib.escapeShellArg key} >/dev/null echo Key ${lib.escapeShellArg key} was created. else echo "Key already exists, assuming ownership" fi ''; destroy = '' garage key delete --yes "$OBJECT" ''; change = key: let kCfg = cfg.keys.${key}; in '' garage key ${if kCfg.allow.createBucket then "allow" else "deny"} ${lib.escapeShellArg key} --create-bucket >/dev/null ''; }; bucket = { deps = [ "key" ]; destroyAfterDays = 30; create = bucket: '' if [[ "$(garage bucket info ${lib.escapeShellArg bucket} 2>&1 >/dev/null)" == "Error: Bucket not found" ]]; then garage bucket create ${lib.escapeShellArg bucket} else echo "Bucket already exists, assuming ownership" fi ''; destroy = '' garage bucket delete --yes "$OBJECT" ''; change = bucket: let bCfg = cfg.buckets.${bucket}; in '' # permissions ${lib.concatStringsSep "\n" (lib.flatten ( lib.mapAttrsToList (key: perms: '' garage bucket allow ${lib.escapeShellArg bucket} --key ${lib.escapeShellArg key} ${lib.escapeShellArgs (map (x: "--${x}") perms)} garage bucket deny ${lib.escapeShellArg bucket} --key ${lib.escapeShellArg key} ${lib.escapeShellArgs (map (x: "--${x}") (lib.subtractLists perms [ "read" "write" "owner" ]))} '') bCfg.allow ))} # quotas garage bucket set-quotas ${lib.escapeShellArg bucket} \ --max-objects '${if bCfg.quotas.maxObjects == null then "none" else toString bCfg.quotas.maxObjects}' \ --max-size '${if bCfg.quotas.maxSize == null then "none" else toString bCfg.quotas.maxSize}' # website access garage bucket website ${if bCfg.web.enable then "--allow" else "--deny"} ${lib.escapeShellArg bucket} ''; }; }; }; services.locksmith.providers.garage = { secrets = lib.mkMerge (lib.mapAttrsToList (key: kCfg: let common = { inherit (kCfg.locksmith) mode owner group nodes; }; getKeyID = "${cfg.package}/bin/garage key info ${lib.escapeShellArg key} | grep -m1 'Key ID:' | cut -d ' ' -f3"; getSecretKey = "${cfg.package}/bin/garage key info ${lib.escapeShellArg key} | grep -m1 'Secret key:' | cut -d ' ' -f3"; in if kCfg.locksmith.format == "files" then { "${key}-id" = common // { command = getKeyID; }; "${key}-secret" = common // { command = getSecretKey; }; } else let template = pkgs.writeText "garage-key-template" { aws = '' [default] aws_access_key_id=@@GARAGE_KEY_ID@@ aws_secret_access_key=@@GARAGE_SECRET_KEY@@ ''; envFile = '' AWS_ACCESS_KEY_ID=@@GARAGE_KEY_ID@@ AWS_SECRET_ACCESS_KEY=@@GARAGE_SECRET_KEY@@ ''; s3ql = '' [s3c] storage-url: s3c4:// backend-login: @@GARAGE_KEY_ID@@ backend-password: @@GARAGE_SECRET_KEY@@ ''; }.${kCfg.locksmith.format}; in { ${key} = common // { command = pkgs.writeShellScript "garage-render-key-template" '' tmpFile="$(mktemp -ut garageKeyTemplate-XXXXXXXXXXXXXXXX)" cp ${template} "$tmpFile" trap "rm -f $tmpFile" EXIT chmod 600 "$tmpFile" ${getKeyID} | ${pkgs.replace-secret}/bin/replace-secret '@@GARAGE_KEY_ID@@' /dev/stdin "$tmpFile" ${getSecretKey} | ${pkgs.replace-secret}/bin/replace-secret '@@GARAGE_SECRET_KEY@@' /dev/stdin "$tmpFile" cat "$tmpFile" ''; }; }) cfg.keys); }; }; }