forked from fediversity/fediversity
		
	closes #93. note that this includes classes: - `nixos` - `nixosTest` - `nixops4Resource` - `nixops4Deployment` .. and my (made-up, as per the [docs](https://ryantm.github.io/nixpkgs/module-system/module-system/#module-system-lib-evalModules-param-class)): - `nix-unit` - `package` .. while i did not manage to cover: - service tests, given `pkgs.nixosTest` seemed to not actually like `_class = "nixosTest"` (?!) ... nor #93's mentioned destructured arguments for that matter, as per Fediversity/Fediversity#93 (comment) - let me know if that is still desired as well. Reviewed-on: Fediversity/Fediversity#398 Reviewed-by: Valentin Gagarin <valentin.gagarin@tweag.io> Co-authored-by: Kiara Grouwstra <kiara@procolix.eu> Co-committed-by: Kiara Grouwstra <kiara@procolix.eu>
		
			
				
	
	
		
			212 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			212 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
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
 | 
						|
 | 
						|
{
 | 
						|
  config,
 | 
						|
  lib,
 | 
						|
  pkgs,
 | 
						|
  ...
 | 
						|
}:
 | 
						|
 | 
						|
let
 | 
						|
  inherit (builtins) toString;
 | 
						|
  inherit (lib) optionalString concatStringsSep mkIf;
 | 
						|
  inherit (lib.strings) escapeShellArg;
 | 
						|
  inherit (lib.attrsets) filterAttrs mapAttrs';
 | 
						|
  concatMapAttrs = scriptFn: attrset: concatStringsSep "\n" (lib.mapAttrsToList scriptFn attrset);
 | 
						|
 | 
						|
  cfg = config.services.garage;
 | 
						|
 | 
						|
  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 ${config.fediversity.garage.api.url} s3api put-bucket-cors --bucket ${bucketArg} --cors-configuration ${corsRulesJSON}
 | 
						|
        garage bucket deny --read --write --owner ${bucketArg} --key tmp
 | 
						|
      ''}
 | 
						|
    '';
 | 
						|
 | 
						|
  ensureBucketsScript = concatMapAttrs ensureBucketScriptFn config.fediversity.garage.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:
 | 
						|
    {
 | 
						|
      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}
 | 
						|
    '';
 | 
						|
 | 
						|
  ensureKeysScript = concatMapAttrs ensureKeyScriptFn config.fediversity.garage.ensureKeys;
 | 
						|
 | 
						|
in
 | 
						|
{
 | 
						|
  _class = "nixos";
 | 
						|
 | 
						|
  imports = [ ./options.nix ];
 | 
						|
 | 
						|
  config = mkIf config.fediversity.garage.enable {
 | 
						|
    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.fediversity.garage.api.port
 | 
						|
      config.fediversity.garage.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.fediversity.garage.rpc.port}";
 | 
						|
        rpc_public_addr = "[::1]:${toString config.fediversity.garage.rpc.port}";
 | 
						|
        s3_api.api_bind_addr = "[::]:${toString config.fediversity.garage.api.port}";
 | 
						|
        s3_web.bind_addr = "[::]:${toString config.fediversity.garage.web.internalPort}";
 | 
						|
        s3_web.root_domain = ".${config.fediversity.garage.web.rootDomain}";
 | 
						|
        index = "index.html";
 | 
						|
 | 
						|
        s3_api.s3_region = "garage";
 | 
						|
        s3_api.root_domain = ".${config.fediversity.garage.api.domain}";
 | 
						|
      };
 | 
						|
    };
 | 
						|
 | 
						|
    services.nginx.enable = true;
 | 
						|
 | 
						|
    ## 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 = "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 = config.fediversity.garage.web.domainForBucket bucket;
 | 
						|
        inherit value;
 | 
						|
      }) (filterAttrs (_: { website, ... }: website) config.fediversity.garage.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 ${config.fediversity.garage.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
 | 
						|
      '';
 | 
						|
    };
 | 
						|
  };
 | 
						|
}
 |