let # generate one using openssl (somehow) # 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 snakeoil_key = { id = "GK22a15201acacbd51cd43e327"; secret = "82b2b4cbef27bf8917b350d5b10a87c92fa9c8b13a415aeeea49726cf335d74e"; }; in # TODO: expand to a multi-machine setup { config, lib, pkgs, ... }: let inherit (builtins) toString; inherit (lib) types mkOption mkEnableOption optionalString concatStringsSep ; inherit (lib.strings) escapeShellArg; inherit (lib.attrsets) filterAttrs mapAttrs'; cfg = config.services.garage; fedicfg = config.fediversity.internal.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} '') aliases )} ${optionalString corsRules.enable '' garage bucket allow --read --write --owner ${bucketArg} --key tmp # TODO: endpoin-url should not be hard-coded aws --region ${cfg.settings.s3_api.s3_region} --endpoint-url ${fedicfg.api.url} s3api put-bucket-cors --bucket ${bucketArg} --cors-configuration ${corsRulesJSON} 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 "--read"} ${optionalString write "--write"} ${optionalString owner "--owner"} \ ${escapeShellArg bucket} --key ${escapeShellArg key} ''; ensureKeyScriptFn = key: { id, secret, ensureAccess, }: '' ## 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} || : ${concatMapAttrs (ensureAccessScriptFn key) ensureAccess} ''; ensureKeysScript = concatMapAttrs ensureKeyScriptFn cfg.ensureKeys; in { # add in options to ensure creation of buckets and keys options = { services.garage = { ensureBuckets = mkOption { type = types.attrsOf ( types.submodule { options = { website = mkOption { type = types.bool; default = false; }; # I think setting corsRules should allow another website to show images from your bucket corsRules = { enable = mkEnableOption "CORS Rules"; 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 = [ ]; }; }; } ); default = { }; }; ensureKeys = mkOption { type = types.attrsOf ( types.submodule { # TODO: these should be managed as secrets, not in the nix store options = { id = mkOption { type = types.str; }; secret = mkOption { type = types.str; }; # TODO: assert at least one of these is true # NOTE: this currently needs to be done at the top level module 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 = [ ]; }; }; } ); default = { }; }; }; }; config = lib.mkIf config.fediversity.enable { environment.systemPackages = [ pkgs.minio-client pkgs.awscli ]; networking.firewall.allowedTCPPorts = [ fedicfg.rpc.port ]; services.garage = { enable = true; package = pkgs.garage_0_9; settings = { replication_mode = "none"; # TODO: use a secret file rpc_secret = "d576c4478cc7d0d94cfc127138cbb82018b0155c037d1c827dfb6c36be5f6625"; # TODO: why does this have to be set? is there not a sensible default? rpc_bind_addr = "[::]:${toString fedicfg.rpc.port}"; rpc_public_addr = "[::1]:${toString fedicfg.rpc.port}"; s3_api.api_bind_addr = "[::]:${toString fedicfg.api.port}"; s3_web.bind_addr = "[::]:${toString fedicfg.web.internalPort}"; s3_web.root_domain = ".${fedicfg.web.rootDomain}"; index = "index.html"; s3_api.s3_region = "garage"; s3_api.root_domain = ".${fedicfg.api.domain}"; }; }; ## Create a proxy from .web.garage. to localhost:3902 for ## each bucket that has `website = true`. services.nginx.virtualHosts = let value = { forceSSL = true; enableACME = true; locations."/" = { proxyPass = "http://localhost:3902"; extraConfig = '' ## copied from https://garagehq.deuxfleurs.fr/documentation/cookbook/reverse-proxy/ proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Disable buffering to a temporary file. proxy_max_temp_file_size 0; ## NOTE: This page suggests many more options for the object storage ## proxy. We should take a look. ## https://docs.joinmastodon.org/admin/optional/object-storage-proxy/ ''; }; }; in mapAttrs' (bucket: _: { name = fedicfg.web.domainForBucket bucket; inherit value; }) (filterAttrs (_: { website, ... }: website) cfg.ensureBuckets); systemd.services.ensure-garage = { after = [ "garage.service" ]; wantedBy = [ "garage.service" ]; serviceConfig = { Type = "oneshot"; }; path = [ cfg.package pkgs.perl pkgs.awscli ]; script = '' set -xeuo pipefail # Give Garage time to start up by waiting until somethings speaks HTTP # behind Garage's API URL. until ${pkgs.curl}/bin/curl -sio /dev/null ${fedicfg.api.url}; do sleep 1; done # XXX: this is very sensitive to being a single instance # (doing the bare minimum to get garage up and running) # also, it's crazy that we have to parse command output like this # 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 GARAGE_ID=$(garage node id 2>/dev/null | perl -ne '/(.*)@.*/ && print $1') garage layout assign -z g1 -c 1G $GARAGE_ID LAYOUT_VER=$(garage layout show | perl -ne '/Current cluster layout version: (\d*)/ && print $1') garage layout apply --version $((LAYOUT_VER + 1)) # XXX: this is a hack because we want to write to the buckets here but we're not guaranteed any access keys # TODO: generate this key here rather than using a well-known key # 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} || : export AWS_ACCESS_KEY_ID=${snakeoil_key.id}; export AWS_SECRET_ACCESS_KEY=${snakeoil_key.secret}; ${ensureBucketsScript} ${ensureKeysScript} # garage doesn't like re-adding keys that once existed, so we can't delete / recreate it every time # garage key delete ${snakeoil_key.id} --yes ''; }; }; }