forked from fediversity/fediversity
370 lines
16 KiB
Nix
370 lines
16 KiB
Nix
{ pkgs, lib, ... }:
|
|
let
|
|
inherit (lib) mkEnableOption mkOption types;
|
|
in
|
|
{
|
|
resources.garage = {
|
|
description = "Garage storage configuration for an application.";
|
|
request =
|
|
{ ... }:
|
|
{
|
|
_class = "fediversity-resource-request";
|
|
options = {
|
|
enable = mkEnableOption "Enable a Garage server on the machine";
|
|
nixos-configuration = mkOption {
|
|
description = "The NixOS configuration module to include.";
|
|
type = types.functionTo types.unspecified;
|
|
default = _: { };
|
|
};
|
|
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 {
|
|
options = {
|
|
s3AccessKeyFile = mkOption { type = types.path; };
|
|
s3SecretKeyFile = mkOption { type = types.path; };
|
|
# 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 = { };
|
|
};
|
|
};
|
|
};
|
|
policy =
|
|
{ config, ... }:
|
|
{
|
|
_class = "fediversity-resource-policy";
|
|
options = {
|
|
enable = lib.mkEnableOption "S3-compatible storage provider Garage";
|
|
rpc = {
|
|
port = mkOption {
|
|
type = types.int;
|
|
default = 3901;
|
|
};
|
|
};
|
|
api = {
|
|
domain = mkOption {
|
|
type = types.str;
|
|
# default = "s3.garage.${config.fediversity.domain}";
|
|
default = "s3.garage.fediversity.net";
|
|
};
|
|
port = mkOption {
|
|
type = types.int;
|
|
default = 3900;
|
|
};
|
|
url = mkOption {
|
|
type = types.str;
|
|
default = "http://${config.api.domain}:${toString config.api.port}";
|
|
};
|
|
};
|
|
web = {
|
|
rootDomain = mkOption {
|
|
type = types.str;
|
|
# default = "web.garage.${config.fediversity.domain}";
|
|
default = "web.garage.fediversity.net";
|
|
};
|
|
internalPort = mkOption {
|
|
type = types.int;
|
|
default = 3902;
|
|
};
|
|
domainForBucket = mkOption {
|
|
type = types.functionTo types.str;
|
|
default = bucket: "${bucket}.${config.web.rootDomain}";
|
|
};
|
|
urlForBucket = mkOption {
|
|
type = types.functionTo types.str;
|
|
default = bucket: "http://${config.web.domainForBucket bucket}";
|
|
};
|
|
};
|
|
};
|
|
config = {
|
|
resource-type = types.submodule {
|
|
options = {
|
|
garageSide = mkOption {
|
|
type = types.listOf types.unspecified;
|
|
default = [ ];
|
|
};
|
|
applicationSide = mkOption {
|
|
type = types.listOf types.unspecified;
|
|
default = [ ];
|
|
};
|
|
mainConfig = mkOption {
|
|
type = types.unspecified;
|
|
default = { };
|
|
};
|
|
};
|
|
};
|
|
apply = requests: {
|
|
applicationSide = map (req: req.nixos-configuration config) requests;
|
|
|
|
mainConfig = nixos: {
|
|
config = {
|
|
environment.systemPackages = [
|
|
pkgs.minio-client
|
|
pkgs.awscli
|
|
];
|
|
## REVIEW: Do we want to reverse proxy the RPC and API ports? In fact,
|
|
## shouldn't we just get rid of RPC at all, we're not using it.
|
|
networking.firewall.allowedTCPPorts = [
|
|
80
|
|
443
|
|
config.api.port
|
|
config.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 config.rpc.port}";
|
|
rpc_public_addr = "[::1]:${toString config.rpc.port}";
|
|
s3_api.api_bind_addr = "[::]:${toString config.api.port}";
|
|
s3_web.bind_addr = "[::]:${toString config.web.internalPort}";
|
|
s3_web.root_domain = ".${config.web.rootDomain}";
|
|
index = "index.html";
|
|
s3_api.s3_region = "garage";
|
|
s3_api.root_domain = ".${config.api.domain}";
|
|
};
|
|
};
|
|
services.nginx.enable = true;
|
|
systemd.services.ensure-garage = {
|
|
after = [ "garage.service" ];
|
|
wantedBy = [ "garage.service" ];
|
|
serviceConfig.Type = "oneshot";
|
|
path = [
|
|
nixos.config.services.garage.package
|
|
pkgs.perl
|
|
pkgs.awscli
|
|
];
|
|
};
|
|
|
|
};
|
|
};
|
|
|
|
garageSide =
|
|
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";
|
|
};
|
|
inherit (lib)
|
|
optionals
|
|
optionalString
|
|
concatStringsSep
|
|
lists
|
|
strings
|
|
attrsets
|
|
;
|
|
inherit (strings) escapeShellArg;
|
|
inherit (attrsets) filterAttrs mapAttrs';
|
|
inherit (lists) map;
|
|
concatMapAttrs = scriptFn: attrset: concatStringsSep "\n" (lib.mapAttrsToList scriptFn attrset);
|
|
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:
|
|
{
|
|
s3AccessKeyFile,
|
|
s3SecretKeyFile,
|
|
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} $(cat ${escapeShellArg s3AccessKeyFile}) $(cat ${escapeShellArg s3SecretKeyFile}) || :
|
|
${concatMapAttrs (ensureAccessScriptFn key) ensureAccess}
|
|
'';
|
|
ensureBucketScriptFn =
|
|
cfg: 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: endpoint-url should not be hard-coded
|
|
aws --region ${cfg.settings.s3_api.s3_region} --endpoint-url ${config.api.url} s3api put-bucket-cors --bucket ${bucketArg} --cors-configuration ${corsRulesJSON}
|
|
garage bucket deny --read --write --owner ${bucketArg} --key tmp
|
|
''}
|
|
'';
|
|
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/
|
|
'';
|
|
};
|
|
};
|
|
ensureBucketsScript =
|
|
cfg:
|
|
lib.concatStringsSep "\n" (
|
|
map ({ ensureBuckets, ... }: concatMapAttrs (ensureBucketScriptFn cfg) ensureBuckets) requests
|
|
);
|
|
ensureKeysScript = lib.concatStringsSep "\n" (
|
|
map ({ ensureKeys, ... }: concatMapAttrs ensureKeyScriptFn ensureKeys) requests
|
|
);
|
|
baseCfg = nixos: {
|
|
config = {
|
|
systemd.services.ensure-garage = {
|
|
after = [ "garage.service" ];
|
|
wantedBy = [ "garage.service" ];
|
|
serviceConfig.Type = "oneshot";
|
|
path = [
|
|
nixos.config.services.garage.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 ${config.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 nixos.config.services.garage}
|
|
${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
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
in
|
|
optionals config.enable (
|
|
[
|
|
baseCfg
|
|
]
|
|
++ (map (
|
|
{ ensureBuckets, ... }:
|
|
{
|
|
## Create a proxy from <bucket>.web.garage.<domain> to localhost:3902 for
|
|
## each bucket that has `website = true`.
|
|
services.nginx.virtualHosts = mapAttrs' (bucket: _: {
|
|
name = config.web.domainForBucket bucket;
|
|
inherit value;
|
|
}) (filterAttrs (_: { website, ... }: website) ensureBuckets);
|
|
}
|
|
) requests)
|
|
);
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|