Unverified Commit 86420f4e authored by Adam C. Stephens's avatar Adam C. Stephens Committed by GitHub
Browse files

nixos/atticd: init module (#347749)

parents 7fa916ff 8d4f3f2b
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -980,6 +980,7 @@
  ./services/networking/aria2.nix
  ./services/networking/asterisk.nix
  ./services/networking/atftpd.nix
  ./services/networking/atticd.nix
  ./services/networking/autossh.nix
  ./services/networking/avahi-daemon.nix
  ./services/networking/babeld.nix
+233 −0
Original line number Diff line number Diff line
{
  lib,
  pkgs,
  config,
  ...
}:

let
  inherit (lib) types;

  cfg = config.services.atticd;

  format = pkgs.formats.toml { };

  checkedConfigFile =
    pkgs.runCommand "checked-attic-server.toml"
      {
        configFile = format.generate "server.toml" cfg.settings;
      }
      ''
        export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="$(${lib.getExe pkgs.openssl} genrsa -traditional 4096 | ${pkgs.coreutils}/bin/base64 -w0)"
        export ATTIC_SERVER_DATABASE_URL="sqlite://:memory:"
        ${lib.getExe cfg.package} --mode check-config -f $configFile
        cat <$configFile >$out
      '';

  atticadmShim = pkgs.writeShellScript "atticadm" ''
    if [ -n "$ATTICADM_PWD" ]; then
      cd "$ATTICADM_PWD"
      if [ "$?" != "0" ]; then
        >&2 echo "Warning: Failed to change directory to $ATTICADM_PWD"
      fi
    fi

    exec ${cfg.package}/bin/atticadm -f ${checkedConfigFile} "$@"
  '';

  atticadmWrapper = pkgs.writeShellScriptBin "atticd-atticadm" ''
    exec systemd-run \
      --quiet \
      --pipe \
      --pty \
      --same-dir \
      --wait \
      --collect \
      --service-type=exec \
      --property=EnvironmentFile=${cfg.environmentFile} \
      --property=DynamicUser=yes \
      --property=User=${cfg.user} \
      --property=Environment=ATTICADM_PWD=$(pwd) \
      --working-directory / \
      -- \
      ${atticadmShim} "$@"
  '';

  hasLocalPostgresDB =
    let
      url = cfg.settings.database.url or "";
      localStrings = [
        "localhost"
        "127.0.0.1"
        "/run/postgresql"
      ];
      hasLocalStrings = lib.any (lib.flip lib.hasInfix url) localStrings;
    in
    config.services.postgresql.enable && lib.hasPrefix "postgresql://" url && hasLocalStrings;
in
{
  options = {
    services.atticd = {
      enable = lib.mkEnableOption "the atticd, the Nix Binary Cache server";

      package = lib.mkPackageOption pkgs "attic-server" { };

      environmentFile = lib.mkOption {
        description = ''
          Path to an EnvironmentFile containing required environment
          variables:

          - ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64: The base64-encoded RSA PEM PKCS1 of the
            RS256 JWT secret. Generate it with `openssl genrsa -traditional 4096 | base64 -w0`.
        '';
        type = types.nullOr types.path;
        default = null;
      };

      user = lib.mkOption {
        description = ''
          The group under which attic runs.
        '';
        type = types.str;
        default = "atticd";
      };

      group = lib.mkOption {
        description = ''
          The user under which attic runs.
        '';
        type = types.str;
        default = "atticd";
      };

      settings = lib.mkOption {
        description = ''
          Structured configurations of atticd.
          See https://github.com/zhaofengli/attic/blob/main/server/src/config-template.toml
        '';
        type = format.type;
        default = { };
      };

      mode = lib.mkOption {
        description = ''
          Mode in which to run the server.

          'monolithic' runs all components, and is suitable for single-node deployments.

          'api-server' runs only the API server, and is suitable for clustering.

          'garbage-collector' only runs the garbage collector periodically.

          A simple NixOS-based Attic deployment will typically have one 'monolithic' and any number of 'api-server' nodes.

          There are several other supported modes that perform one-off operations, but these are the only ones that make sense to run via the NixOS module.
        '';
        type = lib.types.enum [
          "monolithic"
          "api-server"
          "garbage-collector"
        ];
        default = "monolithic";
      };
    };
  };

  config = lib.mkIf cfg.enable {
    assertions = [
      {
        assertion = cfg.environmentFile != null;
        message = ''
          <option>services.atticd.environmentFile</option> is not set.

          Run `openssl genrsa -traditional 4096 | base64 -w0` and create a file with the following contents:

          ATTIC_SERVER_TOKEN_RS256_SECRET="output from command"

          Then, set `services.atticd.environmentFile` to the quoted absolute path of the file.
        '';
      }
    ];

    services.atticd.settings = {
      chunking = lib.mkDefault {
        nar-size-threshold = 65536;
        min-size = 16384; # 16 KiB
        avg-size = 65536; # 64 KiB
        max-size = 262144; # 256 KiB
      };

      database.url = lib.mkDefault "sqlite:///var/lib/atticd/server.db?mode=rwc";

      # "storage" is internally tagged
      # if the user sets something the whole thing must be replaced
      storage = lib.mkDefault {
        type = "local";
        path = "/var/lib/atticd/storage";
      };
    };

    systemd.services.atticd = {
      wantedBy = [ "multi-user.target" ];
      after = [ "network-online.target" ] ++ lib.optionals hasLocalPostgresDB [ "postgresql.service" ];
      requires = lib.optionals hasLocalPostgresDB [ "postgresql.service" ];

      serviceConfig = {
        ExecStart = "${lib.getExe cfg.package} -f ${checkedConfigFile} --mode ${cfg.mode}";
        EnvironmentFile = cfg.environmentFile;
        StateDirectory = "atticd"; # for usage with local storage and sqlite
        DynamicUser = true;
        User = cfg.user;
        Group = cfg.group;
        Restart = "on-failure";
        RestartSec = 10;

        CapabilityBoundingSet = [ "" ];
        DeviceAllow = "";
        DevicePolicy = "closed";
        LockPersonality = true;
        MemoryDenyWriteExecute = true;
        NoNewPrivileges = true;
        PrivateDevices = true;
        PrivateTmp = true;
        PrivateUsers = true;
        ProcSubset = "pid";
        ProtectClock = true;
        ProtectControlGroups = true;
        ProtectHome = true;
        ProtectHostname = true;
        ProtectKernelLogs = true;
        ProtectKernelModules = true;
        ProtectKernelTunables = true;
        ProtectProc = "invisible";
        ProtectSystem = "strict";
        ReadWritePaths =
          let
            path = cfg.settings.storage.path;
            isDefaultStateDirectory = path == "/var/lib/atticd" || lib.hasPrefix "/var/lib/atticd/" path;
          in
          lib.optionals (cfg.settings.storage.type or "" == "local" && !isDefaultStateDirectory) [ path ];
        RemoveIPC = true;
        RestrictAddressFamilies = [
          "AF_INET"
          "AF_INET6"
          "AF_UNIX"
        ];
        RestrictNamespaces = true;
        RestrictRealtime = true;
        RestrictSUIDSGID = true;
        SystemCallArchitectures = "native";
        SystemCallFilter = [
          "@system-service"
          "~@resources"
          "~@privileged"
        ];
        UMask = "0077";
      };
    };

    environment.systemPackages = [
      atticadmWrapper
    ];
  };
}
+1 −0
Original line number Diff line number Diff line
@@ -136,6 +136,7 @@ in {
  artalk = handleTest ./artalk.nix {};
  atd = handleTest ./atd.nix {};
  atop = handleTest ./atop.nix {};
  atticd = runTest ./atticd.nix;
  atuin = handleTest ./atuin.nix {};
  audiobookshelf = handleTest ./audiobookshelf.nix {};
  auth-mysql = handleTest ./auth-mysql.nix {};

nixos/tests/atticd.nix

0 → 100644
+92 −0
Original line number Diff line number Diff line
{ lib, pkgs, ... }:

let
  accessKey = "BKIKJAA5BMMU2RHO6IBB";
  secretKey = "V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12";

  minioCredentialsFile = pkgs.writeText "minio-credentials-full" ''
    MINIO_ROOT_USER=${accessKey}
    MINIO_ROOT_PASSWORD=${secretKey}
  '';
  environmentFile = pkgs.runCommand "atticd-env" { } ''
    echo ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="$(${lib.getExe pkgs.openssl} genrsa -traditional 4096 | ${pkgs.coreutils}/bin/base64 -w0)" > $out
  '';
in

{
  name = "atticd";

  nodes = {
    local = {
      services.atticd = {
        enable = true;

        inherit environmentFile;
      };

      environment.systemPackages = [
        pkgs.attic-client
      ];
    };

    s3 = {
      services.atticd = {
        enable = true;
        settings = {
          storage = {
            type = "s3";
            bucket = "attic";
            region = "us-east-1";
            endpoint = "http://127.0.0.1:9000";

            credentials = {
              access_key_id = accessKey;
              secret_access_key = secretKey;
            };
          };
        };

        inherit environmentFile;
      };

      services.minio = {
        enable = true;
        rootCredentialsFile = minioCredentialsFile;
      };

      environment.systemPackages = [
        pkgs.attic-client
        pkgs.minio-client
      ];
    };
  };

  testScript = # python
    ''
      start_all()

      with subtest("local storage push"):
          local.wait_for_unit("atticd.service")
          token = local.succeed("atticd-atticadm make-token --sub stop --validity 1y --create-cache '*' --pull '*' --push '*' --delete '*' --configure-cache '*' --configure-cache-retention '*'").strip()

          local.succeed(f"attic login local http://localhost:8080 {token}")
          local.succeed("attic cache create test-cache")
          local.succeed("attic push test-cache ${environmentFile}")

      with subtest("s3 storage push"):
          s3.wait_for_unit("atticd.service")
          s3.wait_for_unit("minio.service")
          s3.wait_for_open_port(9000)
          s3.succeed(
              "mc config host add minio "
              + "http://localhost:9000 "
              + "${accessKey} ${secretKey} --api s3v4",
              "mc mb minio/attic",
          )
          token = s3.succeed("atticd-atticadm make-token --sub stop --validity 1y --create-cache '*' --pull '*' --push '*' --delete '*' --configure-cache '*' --configure-cache-retention '*'").strip()

          s3.succeed(f"attic login s3 http://localhost:8080 {token}")
          s3.succeed("attic cache create test-cache")
          s3.succeed("attic push test-cache ${environmentFile}")
    '';
}
+8 −1
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@
, rustPlatform
, fetchFromGitHub
, nix
, nixosTests
, boost
, pkg-config
, stdenv
@@ -53,7 +54,13 @@ rustPlatform.buildRustPackage {
    fi
  '';

  passthru.updateScript = ./update.sh;
  passthru = {
    tests = {
      inherit (nixosTests) atticd;
    };

    updateScript = ./update.sh;
  };

  meta = with lib; {
    description = "Multi-tenant Nix Binary Cache";