2024-03-27 10:42:11 +01:00
let
# generate one using openssl (somehow)
2024-05-25 01:02:12 +02:00
# XXX: when importing, garage tells you importing is only meant for keys previously generated by garage. is it okay to generate them using openssl? it seems to work fine
2024-03-27 10:42:11 +01:00
snakeoil_key = {
2024-04-03 14:40:19 +02:00
id = " G K 2 2 a 1 5 2 0 1 a c a c b d 5 1 c d 4 3 e 3 2 7 " ;
secret = " 8 2 b 2 b 4 c b e f 2 7 b f 8 9 1 7 b 3 5 0 d 5 b 1 0 a 8 7 c 9 2 f a 9 c 8 b 1 3 a 4 1 5 a e e e a 4 9 7 2 6 c f 3 3 5 d 7 4 e " ;
2024-03-27 10:42:11 +01:00
} ;
in
2024-09-17 14:30:59 +02:00
2024-03-27 10:42:11 +01:00
# TODO: expand to a multi-machine setup
2024-09-17 14:30:59 +02:00
{ config , lib , pkgs , . . . }:
2024-06-06 03:37:06 +02:00
let
2024-09-17 17:31:58 +02:00
inherit ( builtins ) toString ;
2024-06-06 03:37:06 +02:00
inherit ( lib ) types mkOption mkEnableOption optionalString concatStringsSep ;
inherit ( lib . strings ) escapeShellArg ;
2024-10-01 18:18:47 +02:00
inherit ( lib . attrsets ) filterAttrs mapAttrs' ;
2024-06-06 03:37:06 +02:00
cfg = config . services . garage ;
2024-09-23 18:09:16 +02:00
fedicfg = config . fediversity . internal . garage ;
2024-06-06 03:37:06 +02:00
concatMapAttrs = scriptFn : attrset : concatStringsSep " \n " ( lib . mapAttrsToList scriptFn attrset ) ;
ensureBucketScriptFn = bucket : { website , aliases , corsRules }:
let
bucketArg = escapeShellArg bucket ;
corsRulesJSON = escapeShellArg ( builtins . toJSON {
CORSRules = [ {
AllowedHeaders = corsRules . allowedHeaders ;
AllowedMethods = corsRules . allowedMethods ;
AllowedOrigins = corsRules . allowedOrigins ;
} ] ;
} ) ;
in ''
# garage bucket info tells us if the bucket already exists
garage bucket info $ { bucketArg } || garage bucket create $ { bucketArg }
# TODO: should this --deny the website if `website` is false?
$ { optionalString website ''
garage bucket website - - allow $ { bucketArg }
'' }
$ { concatStringsSep " \n " ( map ( alias : ''
garage bucket alias $ { bucketArg } $ { escapeShellArg alias }
'' ) a l i a s e s ) }
$ { optionalString corsRules . enable ''
garage bucket allow - - read - - write - - owner $ { bucketArg } - - key tmp
2024-07-18 12:44:13 +02:00
# TODO: endpoin-url should not be hard-coded
2024-09-23 18:09:16 +02:00
aws - - region $ { cfg . settings . s3_api . s3_region } - - endpoint-url $ { fedicfg . api . url } s3api put-bucket-cors - - bucket $ { bucketArg } - - cors-configuration $ { corsRulesJSON }
2024-06-06 03:37:06 +02:00
garage bucket deny - - read - - write - - owner $ { bucketArg } - - key tmp
'' }
'' ;
ensureBucketsScript = concatMapAttrs ensureBucketScriptFn cfg . ensureBuckets ;
ensureAccessScriptFn = key : bucket : { read , write , owner }: ''
garage bucket allow $ { optionalString read " - - r e a d " } $ { optionalString write " - - w r i t e " } $ { optionalString owner " - - o w n e r " } \
$ { escapeShellArg bucket } - - key $ { escapeShellArg key }
'' ;
ensureKeyScriptFn = key : { id , secret , ensureAccess }: ''
2024-09-20 18:35:22 +02:00
## FIXME: Check whether the key exist and skip this step if that is the case. Get rid of this `|| :`
garage key import - - yes - n $ { escapeShellArg key } $ { escapeShellArg id } $ { escapeShellArg secret } || :
2024-06-06 03:37:06 +02:00
$ { concatMapAttrs ( ensureAccessScriptFn key ) ensureAccess }
'' ;
ensureKeysScript = concatMapAttrs ensureKeyScriptFn cfg . ensureKeys ;
2024-09-17 14:30:59 +02:00
in
{
2024-03-27 10:42:11 +01:00
# add in options to ensure creation of buckets and keys
2024-06-06 03:37:06 +02:00
options = {
2024-03-27 10:59:50 +01:00
services . garage = {
ensureBuckets = mkOption {
type = types . attrsOf ( types . submodule {
options = {
website = mkOption {
type = types . bool ;
default = false ;
} ;
2024-05-25 01:02:12 +02:00
# I think setting corsRules should allow another website to show images from your bucket
2024-04-03 14:40:19 +02:00
corsRules = {
enable = mkEnableOption " C O R S R u l e s " ;
allowedHeaders = mkOption {
type = types . listOf types . str ;
default = [ ] ;
} ;
allowedMethods = mkOption {
type = types . listOf types . str ;
default = [ ] ;
} ;
allowedOrigins = mkOption {
type = types . listOf types . str ;
default = [ ] ;
} ;
} ;
aliases = mkOption {
type = types . listOf types . str ;
default = [ ] ;
} ;
2024-03-27 10:59:50 +01:00
} ;
} ) ;
2024-09-02 18:08:14 +02:00
default = { } ;
2024-03-27 10:59:50 +01:00
} ;
ensureKeys = mkOption {
type = types . attrsOf ( types . submodule {
2024-04-03 14:40:19 +02:00
# TODO: these should be managed as secrets, not in the nix store
2024-03-27 10:59:50 +01:00
options = {
id = mkOption {
2024-05-25 01:02:12 +02:00
type = types . str ;
2024-03-27 10:59:50 +01:00
} ;
secret = mkOption {
2024-05-25 01:02:12 +02:00
type = types . str ;
2024-03-27 10:59:50 +01:00
} ;
# TODO: assert at least one of these is true
2024-06-06 03:37:06 +02:00
# NOTE: this currently needs to be done at the top level module
2024-03-27 10:59:50 +01:00
ensureAccess = mkOption {
type = types . attrsOf ( types . submodule {
options = {
read = mkOption {
type = types . bool ;
default = false ;
} ;
write = mkOption {
type = types . bool ;
default = false ;
} ;
owner = mkOption {
type = types . bool ;
default = false ;
} ;
} ;
} ) ;
default = [ ] ;
} ;
} ;
} ) ;
2024-09-02 18:08:14 +02:00
default = { } ;
2024-03-27 10:59:50 +01:00
} ;
} ;
} ;
2024-03-27 10:42:11 +01:00
2024-09-17 14:30:59 +02:00
config = lib . mkIf config . fediversity . enable {
2024-07-18 14:22:47 +02:00
environment . systemPackages = [ pkgs . minio-client pkgs . awscli ] ;
2024-03-27 10:42:11 +01:00
2024-09-17 17:31:58 +02:00
networking . firewall . allowedTCPPorts = [
2024-09-23 18:09:16 +02:00
fedicfg . rpc . port
2024-09-17 17:31:58 +02:00
] ;
2024-03-27 10:42:11 +01:00
services . garage = {
enable = true ;
package = pkgs . garage_0_9 ;
settings = {
replication_mode = " n o n e " ;
# TODO: use a secret file
rpc_secret = " d 5 7 6 c 4 4 7 8 c c 7 d 0 d 9 4 c f c 1 2 7 1 3 8 c b b 8 2 0 1 8 b 0 1 5 5 c 0 3 7 d 1 c 8 2 7 d f b 6 c 3 6 b e 5 f 6 6 2 5 " ;
# TODO: why does this have to be set? is there not a sensible default?
2024-09-23 18:09:16 +02:00
rpc_bind_addr = " [ : : ] : ${ toString fedicfg . rpc . port } " ;
rpc_public_addr = " [ : : 1 ] : ${ toString fedicfg . rpc . port } " ;
s3_api . api_bind_addr = " [ : : ] : ${ toString fedicfg . api . port } " ;
2024-09-23 18:11:04 +02:00
s3_web . bind_addr = " [ : : ] : ${ toString fedicfg . web . internalPort } " ;
2024-09-23 18:09:16 +02:00
s3_web . root_domain = " . ${ fedicfg . web . rootDomain } " ;
2024-03-27 10:42:11 +01:00
index = " i n d e x . h t m l " ;
s3_api . s3_region = " g a r a g e " ;
2024-09-23 18:09:16 +02:00
s3_api . root_domain = " . ${ fedicfg . api . domain } " ;
2024-09-23 17:55:54 +02:00
} ;
} ;
2024-09-23 18:39:15 +02:00
2024-10-01 18:18:47 +02:00
## Create a proxy from <bucket>.web.garage.<domain> to localhost:3902 for
## each bucket that has `website = true`.
services . nginx . virtualHosts =
let
value = {
forceSSL = true ;
enableACME = true ;
locations . " / " = {
proxyPass = " h t t p : / / l o c a l h o s t : 3 9 0 2 " ;
extraConfig = ''
2024-10-30 19:37:45 +01:00
## copied from https://garagehq.deuxfleurs.fr/documentation/cookbook/reverse-proxy/
2024-10-01 18:18:47 +02:00
proxy_set_header Host $ host ;
2024-10-30 19:37:45 +01:00
proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for ;
# Disable buffering to a temporary file.
proxy_max_temp_file_size 0 ;
2024-10-01 18:18:47 +02:00
'' ;
} ;
} ;
in mapAttrs'
( bucket : _ : { name = fedicfg . web . domainForBucket bucket ; inherit value ; } )
( filterAttrs ( _ : { website , . . . }: website ) cfg . ensureBuckets ) ;
2024-09-24 14:40:35 +02:00
2024-03-27 10:42:11 +01:00
systemd . services . ensure-garage = {
after = [ " g a r a g e . s e r v i c e " ] ;
wantedBy = [ " g a r a g e . s e r v i c e " ] ;
2024-09-05 15:32:34 +02:00
serviceConfig = {
Type = " o n e s h o t " ;
} ;
2024-06-06 03:37:06 +02:00
path = [ cfg . package pkgs . perl pkgs . awscli ] ;
2024-03-27 10:42:11 +01:00
script = ''
set - xeuo pipefail
2024-09-10 14:41:53 +02:00
2024-09-17 17:31:58 +02:00
# Give Garage time to start up by waiting until somethings speaks HTTP
# behind Garage's API URL.
2024-09-23 18:09:16 +02:00
until $ { pkgs . curl } /bin/curl - sio /dev/null $ { fedicfg . api . url } ; do sleep 1 ; done
2024-03-27 10:59:50 +01:00
2024-03-27 10:42:11 +01:00
# XXX: this is very sensitive to being a single instance
2024-05-25 01:02:12 +02:00
# (doing the bare minimum to get garage up and running)
2024-03-27 10:42:11 +01:00
# also, it's crazy that we have to parse command output like this
2024-05-25 01:02:12 +02:00
# TODO: talk to garage maintainer about making this nicer to work with in Nix
# before I do that though, I should figure out how setting it up across multiple machines will work
2024-03-27 10:42:11 +01:00
GARAGE_ID = $ ( garage node id 2 > /dev/null | perl - ne ' / ( . * ) @ . * / && print $ 1 ' )
garage layout assign - z g1 - c 1 G $ GARAGE_ID
LAYOUT_VER = $ ( garage layout show | perl - ne ' /Current cluster layout version : ( \ d * ) / && print $ 1 ' )
garage layout apply - - version $ ( ( LAYOUT_VER + 1 ) )
2024-04-03 14:40:19 +02:00
# XXX: this is a hack because we want to write to the buckets here but we're not guaranteed any access keys
2024-05-25 01:02:12 +02:00
# TODO: generate this key here rather than using a well-known key
2024-09-20 18:35:22 +02:00
# TODO: if the key already exists, we get an error; hacked with this `|| :` which needs to be removed
garage key import - - yes - n tmp $ { snakeoil_key . id } $ { snakeoil_key . secret } || :
2024-04-03 14:40:19 +02:00
export AWS_ACCESS_KEY_ID = $ { snakeoil_key . id } ;
export AWS_SECRET_ACCESS_KEY = $ { snakeoil_key . secret } ;
2024-06-06 03:37:06 +02:00
$ { ensureBucketsScript }
$ { ensureKeysScript }
2024-05-25 01:02:12 +02:00
2024-06-25 12:39:04 +02:00
# garage doesn't like re-adding keys that once existed, so we can't delete / recreate it every time
2024-06-06 03:37:06 +02:00
# garage key delete ${snakeoil_key.id} --yes
2024-03-27 10:42:11 +01:00
'' ;
} ;
} ;
}