aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--modules/by-name/at/atuin-sync/module.nix45
-rw-r--r--pkgs/by-name/at/atuin-server-only/package.nix22
-rw-r--r--tests/by-name/at/atuin-sync/test.nix206
3 files changed, 273 insertions, 0 deletions
diff --git a/modules/by-name/at/atuin-sync/module.nix b/modules/by-name/at/atuin-sync/module.nix
new file mode 100644
index 0000000..0db2e29
--- /dev/null
+++ b/modules/by-name/at/atuin-sync/module.nix
@@ -0,0 +1,45 @@
+{
+ config,
+ lib,
+ vhackPackages,
+ ...
+}: let
+ cfg = config.vhack.atuin-sync;
+in {
+ options.vhack.atuin-sync = {
+ enable = lib.mkEnableOption "atuin sync server";
+
+ fqdn = lib.mkOption {
+ description = "The fully qualified domain name of this instance.";
+ type = lib.types.str;
+ example = "atuin-sync.atuin.sh";
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ vhack.nginx.enable = true;
+
+ services = {
+ nginx.virtualHosts."${cfg.fqdn}" = {
+ locations."/" = {
+ proxyPass = "http://127.0.0.1:${toString config.services.atuin.port}";
+ recommendedProxySettings = true;
+ };
+
+ enableACME = true;
+ forceSSL = true;
+ };
+
+ atuin = {
+ enable = true;
+ package = vhackPackages.atuin-server-only;
+ host = "127.0.0.1";
+
+ # Nobody knows about the fqdn and even if, they can only upload encrypted blobs.
+ openRegistration = true;
+
+ database.createLocally = true;
+ };
+ };
+ };
+}
diff --git a/pkgs/by-name/at/atuin-server-only/package.nix b/pkgs/by-name/at/atuin-server-only/package.nix
new file mode 100644
index 0000000..ddad2ec
--- /dev/null
+++ b/pkgs/by-name/at/atuin-server-only/package.nix
@@ -0,0 +1,22 @@
+{atuin}:
+atuin.overrideAttrs (final: prev: {
+ pname = prev.pname + "-server-only";
+ inherit (prev) patches;
+
+ # atuin's default features include 'check-updates', which do not make sense
+ # for distribution builds. List all other default features.
+ buildNoDefaultFeatures = true;
+ buildFeatures = [
+ # All of this is client code
+ # "client"
+ # "sync"
+ # "check-update"
+ # "clipboard"
+ # "daemon" # Background daemon for the client
+
+ "server"
+ ];
+
+ # The checks don't compile without the `server` feature
+ doCheck = true;
+})
diff --git a/tests/by-name/at/atuin-sync/test.nix b/tests/by-name/at/atuin-sync/test.nix
new file mode 100644
index 0000000..3e01885
--- /dev/null
+++ b/tests/by-name/at/atuin-sync/test.nix
@@ -0,0 +1,206 @@
+{
+ nixos-lib,
+ pkgsUnstable,
+ nixpkgs-unstable,
+ vhackPackages,
+ pkgs,
+ extraModules,
+ nixLib,
+ ...
+}:
+nixos-lib.runTest {
+ hostPkgs = pkgs;
+ name = "atuin-sync";
+
+ node = {
+ specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+ # Use the nixpkgs as constructed by the `nixpkgs.*` options
+ pkgs = null;
+ };
+
+ nodes = let
+ atuinSession = "01969ec6b8d07e30a9d2df0911fbfe2a";
+ in {
+ acme = {
+ imports = [
+ ../../../common/acme/server.nix
+ ../../../common/dns/client.nix
+ ];
+ };
+ name_server = {nodes, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../common/acme/client.nix
+ ../../../common/dns/server.nix
+ ];
+
+ vhack.dns.zones = {
+ "atuin-sync.server" = {
+ SOA = {
+ nameServer = "ns";
+ adminEmail = "admin@server.com";
+ serial = 2025012301;
+ };
+ useOrigin = false;
+
+ A = [
+ nodes.server.networking.primaryIPAddress
+ ];
+ AAAA = [
+ nodes.server.networking.primaryIPv6Address
+ ];
+ };
+ };
+ };
+
+ server = {config, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../../modules
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+
+ vhack = {
+ persist.enable = true;
+ nginx.enable = true;
+ atuin-sync = {
+ enable = true;
+ fqdn = "atuin-sync.server";
+ };
+ };
+ };
+
+ client1 = {config, ...}: {
+ imports = [
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+
+ environment.sessionVariables.ATUIN_SESSION = atuinSession;
+
+ environment.systemPackages = [
+ pkgs.atuin
+ pkgs.sqlite-interactive
+ ];
+ };
+ client2 = {config, ...}: {
+ imports = [
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+
+ environment.sessionVariables.ATUIN_SESSION = atuinSession;
+
+ environment.systemPackages = [
+ pkgs.atuin
+ pkgs.sqlite-interactive
+ ];
+ };
+ };
+
+ testScript = {nodes, ...}: let
+ syncLogin = pkgs.writeShellScript "login-atuin-sync-account" ''
+ atuin login --username syncy --password password1234 --key "$1"
+ '';
+
+ syncRegister = pkgs.writeShellScript "register-atuin-sync-account" ''
+ atuin register --username syncy --email syncy@email.com --password password1234
+ '';
+
+ mkSyncConfig = pkgs.writeShellScript "register-atuin-sync-account" ''
+ mkdir --parents ~/.config/atuin/
+ cat << EOF > ~/.config/atuin/config.toml
+ sync_address = "https://atuin-sync.server"
+
+ # Use the v2 sync
+ [sync]
+ records = true
+ EOF
+ '';
+
+ runCommandAndRecordInAtuin = pkgs.writeShellScript "run-command-and-record-in-atuin" ''
+ # SPDX-SnippetBegin
+ # SPDX-SnippetCopyrightText: 2023 mentalisttraceur (https://github.com/mentalisttraceur)
+ # Source: https://github.com/atuinsh/atuin/issues/1188#issuecomment-1698354107
+ run_and_record_in_atuin()
+ {
+ local id
+ local status
+ local escaped_command="$(printf '%q ' "$@")"
+ id="$(atuin history start -- "$escaped_command")"
+ "$@"
+ status=$?
+ atuin history end --exit $status "$id"
+ return $status
+ }
+ # SPDX-SnippetEnd
+
+ run_and_record_in_atuin "$@"
+ '';
+
+ acme_scripts = import ../../../common/acme/scripts.nix {inherit pkgs;};
+ in
+ /*
+ python
+ */
+ ''
+ # Start dependencies for the other services
+ acme.start()
+ acme.wait_for_unit("pebble.service")
+ name_server.start()
+ name_server.wait_for_unit("nsd.service")
+
+ # Start actual test
+ start_all()
+
+ with subtest("Add pebble ca key to all services"):
+ for node in [name_server, server, client1, client2]:
+ node.wait_for_unit("network-online.target")
+ node.succeed("${acme_scripts.add_pebble_acme_ca}")
+
+ server.wait_for_unit("atuin.service")
+ server.wait_for_open_port(443)
+
+ # Wait for the server to acquire the acme certificate
+ client1.wait_until_succeeds("curl https://atuin-sync.server")
+
+ with subtest("Setup client syncing"):
+ for client in [client1, client2]:
+ client.succeed("${mkSyncConfig}")
+
+ client1.succeed("${syncRegister}")
+
+ for client in [client1, client2]:
+ # See https://docs.atuin.sh/guide/sync/
+ client.succeed(f"${syncLogin} '{client1.succeed("atuin key")}'")
+
+ with subtest("Can import shell history"):
+ client1.succeed("${runCommandAndRecordInAtuin} echo hi - client 1")
+ client2.succeed("${runCommandAndRecordInAtuin} echo hi - client 2")
+
+ with subtest("Can sync tasks"):
+ for client in [client1, client2]:
+ client.succeed("atuin sync --force")
+ client1.succeed("atuin sync --force")
+
+
+ with subtest("Have correct tasks"):
+ hist1 = client1.succeed("atuin history list --session --format '{command}'").strip().split('\n')
+ hist2 = client2.succeed("atuin history list --session --format '{command}'").strip().split('\n')
+
+ hist1.sort()
+ hist2.sort()
+
+ canonicalHistory = [
+ "echo hi - client 1",
+ "echo hi - client 2"
+ ]
+
+ assert hist1 == hist2, f"The clients don't have the same amount of history items, client1: '{hist1}', client2: '{hist2}'"
+ assert hist1 == canonicalHistory, f"The history is not correct: '{hist1}' vs. '{canonicalHistory}'"
+ '';
+}