diff --git a/.sops.yaml b/.sops.yaml
index 0af0b0d4..43a4b09b 100644
--- a/.sops.yaml
+++ b/.sops.yaml
@@ -5,7 +5,9 @@ creation_rules:
- path_regex: .*/machines/nixps/.*\.enc$
pgp: F88372B24A806FF23BCB3A4E2DDDF8606958B3F9
- path_regex: machines/sruxps/.*\.enc$
- pgp: EFD6F1EDC84D2FA935E38570462054AB8B682702
+ pgp: &sruxps_pgp EFD6F1EDC84D2FA935E38570462054AB8B682702
+- path_regex: modules/gitlab-runner/gitlab-runner-registration.enc
+ pgp: *sruxps_pgp
- path_regex: cachix/.*\.enc$
pgp: >-
2E8D082C324D5F5459CD27E755BF5D49549F04AD,
diff --git a/modules/gitlab-runner.nix b/modules/gitlab-runner.nix
new file mode 100644
index 00000000..3884c219
--- /dev/null
+++ b/modules/gitlab-runner.nix
@@ -0,0 +1,583 @@
+{ config, lib, pkgs, ... }:
+with builtins;
+with lib;
+let
+ cfg = config.services.gitlab-runner;
+ hasDocker = config.virtualisation.docker.enable;
+ hashedServices =
+ mapAttrs'
+ (name: service: nameValuePair
+ "${name}_${config.networking.hostName}_${
+ substring 0 12
+ (hashString "md5" (unsafeDiscardStringContext (toJSON service)))}"
+ service)
+ cfg.services;
+ configPath = "$HOME/.gitlab-runner/config.toml";
+ configureScript = pkgs.writeShellScriptBin "gitlab-runner-configure" (
+ if (cfg.configFile != null) then ''
+ mkdir -p $(dirname ${configPath})
+ cp ${cfg.configFile} ${configPath}
+ # make config file readable by service
+ chown -R --reference=$HOME $(dirname ${configPath})
+ '' else ''
+ export CONFIG_FILE=${configPath}
+
+ mkdir -p $(dirname ${configPath})
+
+ # remove no longer existing services
+ gitlab-runner verify --delete
+
+ # current and desired state
+ NEEDED_SERVICES=$(echo ${concatStringsSep " " (attrNames hashedServices)} | tr " " "\n")
+ REGISTERED_SERVICES=$(gitlab-runner list 2>&1 | grep 'Executor' | awk '{ print $1 }')
+
+ # difference between current and desired state
+ NEW_SERVICES=$(grep -vxF -f <(echo "$REGISTERED_SERVICES") <(echo "$NEEDED_SERVICES") || true)
+ OLD_SERVICES=$(grep -vxF -f <(echo "$NEEDED_SERVICES") <(echo "$REGISTERED_SERVICES") || true)
+
+ # register new services
+ ${concatStringsSep "\n" (mapAttrsToList (name: service: ''
+ if echo "$NEW_SERVICES" | grep -xq ${name}; then
+ bash -c ${escapeShellArg (concatStringsSep " \\\n " ([
+ "set -a && source ${service.registrationConfigFile} &&"
+ "gitlab-runner register"
+ "--non-interactive"
+ "--name ${name}"
+ "--executor ${service.executor}"
+ "--limit ${toString service.limit}"
+ "--request-concurrency ${toString service.requestConcurrency}"
+ "--maximum-timeout ${toString service.maximumTimeout}"
+ ] ++ service.registrationFlags
+ ++ optional (service.buildsDir != null)
+ "--builds-dir ${service.buildsDir}"
+ ++ optional (service.cloneUrl != null)
+ "--clone-url ${service.cloneUrl}"
+ ++ optional (service.preCloneScript != null)
+ "--pre-clone-script ${service.preCloneScript}"
+ ++ optional (service.preBuildScript != null)
+ "--pre-build-script ${service.preBuildScript}"
+ ++ optional (service.postBuildScript != null)
+ "--post-build-script ${service.postBuildScript}"
+ ++ optional (service.tagList != [ ])
+ "--tag-list ${concatStringsSep "," service.tagList}"
+ ++ optional service.runUntagged
+ "--run-untagged"
+ ++ optional service.protected
+ "--access-level ref_protected"
+ ++ optional service.debugTraceDisabled
+ "--debug-trace-disabled"
+ ++ map (e: "--env ${escapeShellArg e}") (mapAttrsToList (name: value: "${name}=${value}") service.environmentVariables)
+ ++ optionals (service.executor == "docker") (
+ assert (
+ assertMsg (service.dockerImage != null)
+ "dockerImage option is required for docker executor (${name})"
+ );
+ [ "--docker-image ${service.dockerImage}" ]
+ ++ optional service.dockerDisableCache
+ "--docker-disable-cache"
+ ++ optional service.dockerPrivileged
+ "--docker-privileged"
+ ++ map (v: "--docker-volumes ${escapeShellArg v}") service.dockerVolumes
+ ++ map (v: "--docker-extra-hosts ${escapeShellArg v}") service.dockerExtraHosts
+ ++ map (v: "--docker-allowed-images ${escapeShellArg v}") service.dockerAllowedImages
+ ++ map (v: "--docker-allowed-services ${escapeShellArg v}") service.dockerAllowedServices
+ ))
+ )} && sleep 1 || exit 1
+ fi
+ '') hashedServices
+ )}
+
+ # unregister old services
+ for NAME in $(echo "$OLD_SERVICES")
+ do
+ [ ! -z "$NAME" ] && gitlab-runner unregister \
+ --name "$NAME" && sleep 1
+ done
+
+ # update global options
+ remarshal --if toml --of json ${configPath} \
+ | jq -cM ${escapeShellArg (concatStringsSep " | " [
+ ".check_interval = ${toJSON cfg.checkInterval}"
+ ".concurrent = ${toJSON cfg.concurrent}"
+ ".sentry_dsn = ${toJSON cfg.sentryDSN}"
+ ".listen_address = ${toJSON cfg.prometheusListenAddress}"
+ ".session_server.listen_address = ${toJSON cfg.sessionServer.listenAddress}"
+ ".session_server.advertise_address = ${toJSON cfg.sessionServer.advertiseAddress}"
+ ".session_server.session_timeout = ${toJSON cfg.sessionServer.sessionTimeout}"
+ "del(.[] | nulls)"
+ "del(.session_server[] | nulls)"
+ ]
+ )} \
+ | remarshal --if json --of toml \
+ | sponge ${configPath}
+
+ # make config file readable by service
+ chown -R --reference=$HOME $(dirname ${configPath})
+ ''
+ );
+ startScript = pkgs.writeShellScriptBin "gitlab-runner-start" ''
+ export CONFIG_FILE=${configPath}
+ exec gitlab-runner run --working-directory $HOME
+ '';
+in
+{
+ options.services.gitlab-runner = {
+ enable = mkEnableOption "Gitlab Runner";
+ configFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Configuration file for gitlab-runner.
+
+ takes precedence over .
+ and will be ignored too.
+
+ This option is deprecated, please use instead.
+ You can use and
+
+ for settings not covered by this module.
+ '';
+ };
+ checkInterval = mkOption {
+ type = types.int;
+ default = 0;
+ example = literalExample "with lib; (length (attrNames config.services.gitlab-runner.services)) * 3";
+ description = ''
+ Defines the interval length, in seconds, between new jobs check.
+ The default value is 3;
+ if set to 0 or lower, the default value will be used.
+ See runner documentation for more information.
+ '';
+ };
+ concurrent = mkOption {
+ type = types.int;
+ default = 1;
+ example = literalExample "config.nix.maxJobs";
+ description = ''
+ Limits how many jobs globally can be run concurrently.
+ The most upper limit of jobs using all defined runners.
+ 0 does not mean unlimited.
+ '';
+ };
+ sentryDSN = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "https://public:private@host:port/1";
+ description = ''
+ Data Source Name for tracking of all system level errors to Sentry.
+ '';
+ };
+ prometheusListenAddress = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "localhost:8080";
+ description = ''
+ Address (<host>:<port>) on which the Prometheus metrics HTTP server
+ should be listening.
+ '';
+ };
+ sessionServer = mkOption {
+ type = types.submodule {
+ options = {
+ listenAddress = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "0.0.0.0:8093";
+ description = ''
+ An internal URL to be used for the session server.
+ '';
+ };
+ advertiseAddress = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "runner-host-name.tld:8093";
+ description = ''
+ The URL that the Runner will expose to GitLab to be used
+ to access the session server.
+ Fallbacks to if not defined.
+ '';
+ };
+ sessionTimeout = mkOption {
+ type = types.int;
+ default = 1800;
+ description = ''
+ How long in seconds the session can stay active after
+ the job completes (which will block the job from finishing).
+ '';
+ };
+ };
+ };
+ default = { };
+ example = literalExample ''
+ {
+ listenAddress = "0.0.0.0:8093";
+ }
+ '';
+ description = ''
+ The session server allows the user to interact with jobs
+ that the Runner is responsible for. A good example of this is the
+ interactive web terminal.
+ '';
+ };
+ gracefulTermination = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Finish all remaining jobs before stopping.
+ If not set gitlab-runner will stop immediatly without waiting
+ for jobs to finish, which will lead to failed builds.
+ '';
+ };
+ gracefulTimeout = mkOption {
+ type = types.str;
+ default = "infinity";
+ example = "5min 20s";
+ description = ''
+ Time to wait until a graceful shutdown is turned into a forceful one.
+ '';
+ };
+ package = mkOption {
+ type = types.package;
+ default = pkgs.gitlab-runner;
+ defaultText = "pkgs.gitlab-runner";
+ example = literalExample "pkgs.gitlab-runner_1_11";
+ description = "Gitlab Runner package to use.";
+ };
+ extraPackages = mkOption {
+ type = types.listOf types.package;
+ default = [ ];
+ description = ''
+ Extra packages to add to PATH for the gitlab-runner process.
+ '';
+ };
+ services = mkOption {
+ description = "GitLab Runner services.";
+ default = { };
+ example = literalExample ''
+ {
+ # runner for building in docker via host's nix-daemon
+ # nix store will be readable in runner, might be insecure
+ nix = {
+ # File should contain at least these two variables:
+ # `CI_SERVER_URL`
+ # `REGISTRATION_TOKEN`
+ registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+ dockerImage = "alpine";
+ dockerVolumes = [
+ "/nix/store:/nix/store:ro"
+ "/nix/var/nix/db:/nix/var/nix/db:ro"
+ "/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket:ro"
+ ];
+ dockerDisableCache = true;
+ preBuildScript = pkgs.writeScript "setup-container" '''
+ mkdir -p -m 0755 /nix/var/log/nix/drvs
+ mkdir -p -m 0755 /nix/var/nix/gcroots
+ mkdir -p -m 0755 /nix/var/nix/profiles
+ mkdir -p -m 0755 /nix/var/nix/temproots
+ mkdir -p -m 0755 /nix/var/nix/userpool
+ mkdir -p -m 1777 /nix/var/nix/gcroots/per-user
+ mkdir -p -m 1777 /nix/var/nix/profiles/per-user
+ mkdir -p -m 0755 /nix/var/nix/profiles/per-user/root
+ mkdir -p -m 0700 "$HOME/.nix-defexpr"
+
+ . ''${pkgs.nix}/etc/profile.d/nix.sh
+
+ ''${pkgs.nix}/bin/nix-env -i ''${concatStringsSep " " (with pkgs; [ nix cacert git openssh ])}
+
+ ''${pkgs.nix}/bin/nix-channel --add https://nixos.org/channels/nixpkgs-unstable
+ ''${pkgs.nix}/bin/nix-channel --update nixpkgs
+ ''';
+ environmentVariables = {
+ ENV = "/etc/profile";
+ USER = "root";
+ NIX_REMOTE = "daemon";
+ PATH = "/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/bin:/sbin:/usr/bin:/usr/sbin";
+ NIX_SSL_CERT_FILE = "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt";
+ };
+ tagList = [ "nix" ];
+ };
+ # runner for building docker images
+ docker-images = {
+ # File should contain at least these two variables:
+ # `CI_SERVER_URL`
+ # `REGISTRATION_TOKEN`
+ registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+ dockerImage = "docker:stable";
+ dockerVolumes = [
+ "/var/run/docker.sock:/var/run/docker.sock"
+ ];
+ tagList = [ "docker-images" ];
+ };
+ # runner for executing stuff on host system (very insecure!)
+ # make sure to add required packages (including git!)
+ # to `environment.systemPackages`
+ shell = {
+ # File should contain at least these two variables:
+ # `CI_SERVER_URL`
+ # `REGISTRATION_TOKEN`
+ registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+ executor = "shell";
+ tagList = [ "shell" ];
+ };
+ # runner for everything else
+ default = {
+ # File should contain at least these two variables:
+ # `CI_SERVER_URL`
+ # `REGISTRATION_TOKEN`
+ registrationConfigFile = "/run/secrets/gitlab-runner-registration";
+ dockerImage = "debian:stable";
+ };
+ }
+ '';
+ type = types.attrsOf (types.submodule {
+ options = {
+ registrationConfigFile = mkOption {
+ type = types.path;
+ description = ''
+ Absolute path to a file with environment variables
+ used for gitlab-runner registration.
+ A list of all supported environment variables can be found in
+ gitlab-runner register --help.
+
+ Ones that you probably want to set is
+
+ CI_SERVER_URL=<CI server URL>
+
+ REGISTRATION_TOKEN=<registration secret>
+ '';
+ };
+ registrationFlags = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "--docker-helper-image my/gitlab-runner-helper" ];
+ description = ''
+ Extra command-line flags passed to
+ gitlab-runner register.
+ Execute gitlab-runner register --help
+ for a list of supported flags.
+ '';
+ };
+ environmentVariables = mkOption {
+ type = types.attrsOf types.str;
+ default = { };
+ example = { NAME = "value"; };
+ description = ''
+ Custom environment variables injected to build environment.
+ For secrets you can use
+ with RUNNER_ENV variable set.
+ '';
+ };
+ executor = mkOption {
+ type = types.str;
+ default = "docker";
+ description = ''
+ Select executor, eg. shell, docker, etc.
+ See runner documentation for more information.
+ '';
+ };
+ buildsDir = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ example = "/var/lib/gitlab-runner/builds";
+ description = ''
+ Absolute path to a directory where builds will be stored
+ in context of selected executor (Locally, Docker, SSH).
+ '';
+ };
+ cloneUrl = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "http://gitlab.example.local";
+ description = ''
+ Overwrite the URL for the GitLab instance. Used if the Runner can’t connect to GitLab on the URL GitLab exposes itself.
+ '';
+ };
+ dockerImage = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Docker image to be used.
+ '';
+ };
+ dockerVolumes = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "/var/run/docker.sock:/var/run/docker.sock" ];
+ description = ''
+ Bind-mount a volume and create it
+ if it doesn't exist prior to mounting.
+ '';
+ };
+ dockerDisableCache = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Disable all container caching.
+ '';
+ };
+ dockerPrivileged = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Give extended privileges to container.
+ '';
+ };
+ dockerExtraHosts = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "other-host:127.0.0.1" ];
+ description = ''
+ Add a custom host-to-IP mapping.
+ '';
+ };
+ dockerAllowedImages = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "ruby:*" "python:*" "php:*" "my.registry.tld:5000/*:*" ];
+ description = ''
+ Whitelist allowed images.
+ '';
+ };
+ dockerAllowedServices = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "postgres:9" "redis:*" "mysql:*" ];
+ description = ''
+ Whitelist allowed services.
+ '';
+ };
+ preCloneScript = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Runner-specific command script executed before code is pulled.
+ '';
+ };
+ preBuildScript = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Runner-specific command script executed after code is pulled,
+ just before build executes.
+ '';
+ };
+ postBuildScript = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Runner-specific command script executed after code is pulled
+ and just after build executes.
+ '';
+ };
+ tagList = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = ''
+ Tag list.
+ '';
+ };
+ runUntagged = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Register to run untagged builds; defaults to
+ true when is empty.
+ '';
+ };
+ limit = mkOption {
+ type = types.int;
+ default = 0;
+ description = ''
+ Limit how many jobs can be handled concurrently by this service.
+ 0 (default) simply means don't limit.
+ '';
+ };
+ requestConcurrency = mkOption {
+ type = types.int;
+ default = 0;
+ description = ''
+ Limit number of concurrent requests for new jobs from GitLab.
+ '';
+ };
+ maximumTimeout = mkOption {
+ type = types.int;
+ default = 0;
+ description = ''
+ What is the maximum timeout (in seconds) that will be set for
+ job when using this Runner. 0 (default) simply means don't limit.
+ '';
+ };
+ protected = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ When set to true Runner will only run on pipelines
+ triggered on protected branches.
+ '';
+ };
+ debugTraceDisabled = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ When set to true Runner will disable the possibility of
+ using the CI_DEBUG_TRACE feature.
+ '';
+ };
+ };
+ });
+ };
+ };
+ config = mkIf cfg.enable {
+ warnings = optional (cfg.configFile != null) "services.gitlab-runner.`configFile` is deprecated, please use services.gitlab-runner.`services`.";
+ environment.systemPackages = [ cfg.package ];
+ systemd.services.gitlab-runner = {
+ description = "Gitlab Runner";
+ documentation = [ "https://docs.gitlab.com/runner/" ];
+ after = [ "network.target" ]
+ ++ optional hasDocker "docker.service";
+ requires = optional hasDocker "docker.service";
+ wantedBy = [ "multi-user.target" ];
+ environment = config.networking.proxy.envVars // {
+ HOME = "/var/lib/gitlab-runner";
+ };
+ path = with pkgs; [
+ bash
+ gawk
+ jq
+ moreutils
+ remarshal
+ utillinux
+ cfg.package
+ ] ++ cfg.extraPackages;
+ reloadIfChanged = true;
+ serviceConfig = {
+ # Set `DynamicUser` under `systemd.services.gitlab-runner.serviceConfig`
+ # to `lib.mkForce false` in your configuration to run this service as root.
+ # You can also set `User` and `Group` options to run this service as desired user.
+ # Make sure to restart service or changes won't apply.
+ DynamicUser = true;
+ StateDirectory = "gitlab-runner";
+ SupplementaryGroups = optional hasDocker "docker";
+ ExecStartPre = "!${configureScript}/bin/gitlab-runner-configure";
+ ExecStart = "${startScript}/bin/gitlab-runner-start";
+ ExecReload = "!${configureScript}/bin/gitlab-runner-configure";
+ } // optionalAttrs (cfg.gracefulTermination) {
+ TimeoutStopSec = "${cfg.gracefulTimeout}";
+ KillSignal = "SIGQUIT";
+ KillMode = "process";
+ };
+ };
+ # Enable docker if `docker` executor is used in any service
+ virtualisation.docker.enable =
+ mkIf
+ (
+ any (s: s.executor == "docker") (attrValues cfg.services)
+ )
+ (mkDefault true);
+ };
+ imports = [
+ (mkRenamedOptionModule [ "services" "gitlab-runner" "packages" ] [ "services" "gitlab-runner" "extraPackages" ])
+ (mkRemovedOptionModule [ "services" "gitlab-runner" "configOptions" ] "Use services.gitlab-runner.services option instead")
+ (mkRemovedOptionModule [ "services" "gitlab-runner" "workDir" ] "You should move contents of workDir (if any) to /var/lib/gitlab-runner")
+ ];
+}
diff --git a/modules/gitlab-runner/.gitignore b/modules/gitlab-runner/.gitignore
new file mode 100644
index 00000000..1bdd4978
--- /dev/null
+++ b/modules/gitlab-runner/.gitignore
@@ -0,0 +1 @@
+gitlab-runner-registration
diff --git a/modules/gitlab-runner/default.nix b/modules/gitlab-runner/default.nix
new file mode 100644
index 00000000..10a42477
--- /dev/null
+++ b/modules/gitlab-runner/default.nix
@@ -0,0 +1,29 @@
+{ lib, pkgs, ... }:
+let
+ inherit (builtins) concatStringsSep;
+ nixpkgs-unstable = import (import ../nix/sources.nix).nixpkgs-unstable { };
+in
+{
+
+ disabledModules = [
+ "services/continuous-integration/gitlab-runner.nix"
+ ];
+
+ imports = [
+ ../gitlab-runner.nix
+ ];
+
+ # environment.systemPackages = with pkgs; [ git ];
+
+ services.gitlab-runner = {
+ enable = true;
+ services = {
+ shell = {
+ registrationConfigFile = ./gitlab-runner-registration;
+ executor = "shell";
+ tagList = [ "nix-shell" ];
+ };
+ };
+ };
+
+}
diff --git a/modules/gitlab-runner/gitlab-runner-registration.enc b/modules/gitlab-runner/gitlab-runner-registration.enc
new file mode 100644
index 00000000..235c5146
--- /dev/null
+++ b/modules/gitlab-runner/gitlab-runner-registration.enc
@@ -0,0 +1,20 @@
+{
+ "data": "ENC[AES256_GCM,data:zD/nU+qrZYNWI1cmt6K6t1ZuAVhd2sbIgu2djKXWvWYZmnAm1T6pAx2vvG9Gmo0+XuLsYEJP42BTmhslH+NXZ3ff0Bxs+QoTrkDRrJ9TfEOU1y5k,iv:unN+KPHQRK2IYgX1HcMPedlNeVSP50YswnCJpiKgjk0=,tag:gyPW6WAq7t704HhKMsSqgg==,type:str]",
+ "sops": {
+ "kms": null,
+ "gcp_kms": null,
+ "azure_kv": null,
+ "hc_vault": null,
+ "lastmodified": "2020-11-19T07:43:34Z",
+ "mac": "ENC[AES256_GCM,data:x+5U8U50TmXM1AX77oIHU3A0Z5QlJG+v14flFkYEtPuD3hV0kiaLt4G7pfcTbQxTEFGKJe2G7LewQe04syTx6Ycj75EzxsGt1ePWVYzxkvP3kTfgCabTc4BmEW2i7B4A8opYDLHWpDZWAocH5+1kvPDNZ4zu7ExSJlXNUJQbJ70=,iv:YIKeVl8adwvagwRdMua7jc7PvyQ811mNbQvUY1aENfo=,tag:NM7L62fWcIJhfsvIpH3Kvw==,type:str]",
+ "pgp": [
+ {
+ "created_at": "2020-11-19T07:37:48Z",
+ "enc": "-----BEGIN PGP MESSAGE-----\n\nhQIMA9xqWgcQXNt5AQ//XLQZRWdBMB6dgE5zZfgdbov2MZr6AfYf74g/0ESBf5UU\nrQmdMOiYmxkDGznv+nQVVs8h2NVFGvTPptS3u8JUsPaB2yuYb5Q84WV63BmHK+T5\nDPu7ahvEgKqXBGIyDg+4CU8mOQG2gd5A2KLXGqLZoRqSw6PR0VQDIqVRQaV+XaNX\n+ORFFDaH1J9jdzWChLGMIzu5jbgUHHx+TKcMDEuIfa20MKuxp1ZPH+ZYzszHFjS5\nPy1jZqdU+OEH8HawnpjYIJ8U9BpOwXymrFIQ8RBNZMVqXYaHwVdPAtCXV5nRb5OY\n/VyUNKEdWneStSDTWmamnxI5HRa1G/Bwd1NhJl/ityyH7iBsqK9Xm56EIcK0QZi0\n4/1t7Q9yAjmDKWIY970rTe9/jOHM3DaI5/ZHaaRsKkQMZXIn2Kv4tHtIl8xOANLs\nclNn5HglhuFCf8g5IimAtNCsrhycTma3Dg+mJS3lwWa7qupm7hmznOGs/K+J9fHr\nAkteo+qYOy8jPlKQkWMoQf7YOdYlt9bFTtm8qFkzYlwSEBIHf/XEo3rMTVTTiVD7\nPv3/5HfoMA4pNthHpWMNoJRaZe2jHKRhmQIEsjKOrpO70NRi7SuIZwnCid0TK4U4\nxJ9g73mH3jfMNS2zK0q5XEn1w4qPKbzd1twmSE+OKpnVT2K2H1N5joTHRr8GHgTS\nXAEH6aRG8bAHEE4nAwmtqekwvkXSnZp5ukRErzCSA0U4CYmhhvBSKyJs0MUSOSlA\nYQpaziEr2zgaaRrUOFmLVfRILd2yrjhpXcn1MvlKAhD0B6LgaE4lAlc1H/bd\n=n0xq\n-----END PGP MESSAGE-----\n",
+ "fp": "EFD6F1EDC84D2FA935E38570462054AB8B682702"
+ }
+ ],
+ "unencrypted_suffix": "_unencrypted",
+ "version": "3.6.0"
+ }
+}