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 ;
cfg = config . services . garage ;
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-20 17:13:35 +02:00
aws - - region $ { cfg . settings . s3_api . s3_region } - - endpoint-url $ { config . fediversity . internal . garage . 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 }: ''
garage key import - - yes - n $ { escapeShellArg key } $ { escapeShellArg id } $ { escapeShellArg secret }
$ { 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
virtualisation . diskSize = 2048 ;
virtualisation . forwardPorts = [
{
from = " h o s t " ;
2024-09-20 17:13:35 +02:00
host . port = config . fediversity . internal . garage . rpc . port ;
guest . port = config . fediversity . internal . garage . rpc . port ;
2024-07-18 14:22:47 +02:00
}
{
from = " h o s t " ;
2024-09-20 17:13:35 +02:00
host . port = config . fediversity . internal . garage . web . port ;
guest . port = config . fediversity . internal . garage . web . port ;
2024-07-18 14:22:47 +02:00
}
] ;
2024-05-25 01:02:12 +02:00
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-20 17:13:35 +02:00
config . fediversity . internal . garage . rpc . port
config . fediversity . internal . garage . web . 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-20 17:13:35 +02:00
rpc_bind_addr = " [ : : ] : ${ toString config . fediversity . internal . garage . rpc . port } " ;
rpc_public_addr = " [ : : 1 ] : ${ toString config . fediversity . internal . garage . rpc . port } " ;
s3_api . api_bind_addr = " [ : : ] : ${ toString config . fediversity . internal . garage . api . port } " ;
s3_web . bind_addr = " [ : : ] : ${ toString config . fediversity . internal . garage . web . port } " ;
s3_web . root_domain = " . ${ config . fediversity . internal . garage . 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-20 17:13:35 +02:00
s3_api . root_domain = " . ${ config . fediversity . internal . garage . api . domain } " ;
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-20 17:13:35 +02:00
until $ { pkgs . curl } /bin/curl - sio /dev/null $ { config . fediversity . internal . garage . 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-04-03 14:40:19 +02:00
garage key import - - yes - n tmp $ { snakeoil_key . id } $ { snakeoil_key . secret }
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
'' ;
} ;
} ;
}