diff --git a/crane.nix b/crane.nix index c994f3f..30f89f9 100644 --- a/crane.nix +++ b/crane.nix @@ -24,7 +24,7 @@ let version = "0.1.0"; - ignoredPaths = [ ".github" "target" "book" "nixos" ]; + ignoredPaths = [ ".github" "target" "book" "nixos" "integration-tests" ]; src = lib.cleanSourceWith { filter = name: type: !(type == "directory" && builtins.elem (baseNameOf name) ignoredPaths); diff --git a/flake.nix b/flake.nix index 4f0f6ca..7da27bc 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,10 @@ craneLib = crane.mkLib pkgs; in pkgs.callPackage ./crane.nix { inherit craneLib; }; in flake-utils.lib.eachSystem supportedSystems (system: let - pkgs = import nixpkgs { inherit system; }; + pkgs = import nixpkgs { + inherit system; + overlays = []; + }; cranePkgs = makeCranePkgs pkgs; inherit (pkgs) lib; @@ -107,6 +110,14 @@ internal = { inherit (cranePkgs) attic-tests cargoArtifacts; }; + + checks = lib.optionalAttrs pkgs.stdenv.isLinux (import ./integration-tests { + pkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + }; + flake = self; + }); }) // { overlays = { default = final: prev: let diff --git a/integration-tests/README.md b/integration-tests/README.md new file mode 100644 index 0000000..26db1e9 --- /dev/null +++ b/integration-tests/README.md @@ -0,0 +1,3 @@ +# End-to-End Tests + +This directory contains some end-to-end tests for Attic. diff --git a/integration-tests/basic/default.nix b/integration-tests/basic/default.nix new file mode 100644 index 0000000..036b98f --- /dev/null +++ b/integration-tests/basic/default.nix @@ -0,0 +1,204 @@ +{ pkgs, lib, config, flake, attic, ... }: +let + inherit (lib) types; + + serverConfigFile = config.nodes.server.services.atticd.configFile; + + cmd = { + atticadm = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64 && atticadm -f ${serverConfigFile}"; + atticd = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64 && atticd -f ${serverConfigFile}"; + }; + + testDrv = pkgs.writeText "test.nix" '' + #!/bin/sh + /*/sh -c "echo hello > $out"; exit 0; */ + derivation { + name = "hello.txt"; + builder = ./test.nix; + system = builtins.currentSystem; + preferLocalBuild = true; + allowSubstitutes = false; + } + ''; + + databaseModules = { + sqlite = {}; + postgres = { + server = { + services.postgresql = { + enable = true; + ensureDatabases = [ "attic" ]; + ensureUsers = [ + { + name = "atticd"; + ensurePermissions = { + "DATABASE attic" = "ALL PRIVILEGES"; + }; + } + + # For testing only - Don't actually do this + { + name = "root"; + ensureClauses = { + superuser = true; + }; + } + ]; + }; + + services.atticd.settings = { + database.url = "postgresql:///attic?host=/run/postgresql"; + }; + }; + }; + }; + + storageModules = { + local = {}; + minio = let + accessKey = "legit"; + secretKey = "111-1111111"; + in { + server = { + services.minio = { + enable = true; + rootCredentialsFile = "/etc/minio.env"; + }; + + # For testing only - Don't actually do this + environment.etc."minio.env".text = '' + MINIO_ROOT_USER=${accessKey} + MINIO_ROOT_PASSWORD=${secretKey} + ''; + + networking.firewall.allowedTCPPorts = [ 9000 ]; + + services.atticd.settings = { + storage = { + type = "s3"; + endpoint = "http://server:9000"; + region = "us-east-1"; + bucket = "attic"; + credentials = { + access_key_id = accessKey; + secret_access_key = secretKey; + }; + }; + }; + }; + testScript = '' + server.succeed("mkdir /var/lib/minio/data/attic") + server.succeed("chown minio: /var/lib/minio/data/attic") + client.wait_until_succeeds("curl http://server:9000", timeout=10) + ''; + }; + }; +in { + options = { + database = lib.mkOption { + type = types.enum [ "sqlite" "postgres" ]; + default = "sqlite"; + }; + storage = lib.mkOption { + type = types.enum [ "local" "minio" ]; + default = "local"; + }; + }; + + config = { + name = "basic-${config.database}-${config.storage}"; + + nodes = { + server = { + imports = [ + flake.nixosModules.atticd + (databaseModules.${config.database}.server or {}) + (storageModules.${config.storage}.server or {}) + ]; + + # For testing only - Don't actually do this + environment.etc."atticd.env".text = '' + ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="dGVzdCBzZWNyZXQ=" + ''; + + services.atticd = { + enable = true; + credentialsFile = "/etc/atticd.env"; + settings = { + listen = "[::]:8080"; + }; + }; + + networking.firewall.allowedTCPPorts = [ 8080 ]; + }; + + client = { + environment.systemPackages = [ pkgs.attic ]; + }; + }; + + testScript = '' + import time + + start_all() + + ${databaseModules.${config.database}.testScript or ""} + ${storageModules.${config.storage}.testScript or ""} + + server.wait_for_unit('atticd.service') + client.wait_until_succeeds("curl -sL http://server:8080", timeout=10) + + root_token = server.succeed("${cmd.atticadm} make-token --sub 'e2e-root' --validity '1 month' --push '*' --pull '*' --delete '*' --create-cache '*' --destroy-cache '*' --configure-cache '*' --configure-cache-retention '*'") + readonly_token = server.succeed("${cmd.atticadm} make-token --sub 'e2e-root' --validity '1 month' --pull 'test'") + + client.succeed(f"attic login --set-default root http://server:8080 {root_token}") + client.succeed(f"attic login readonly http://server:8080 {readonly_token}") + client.succeed("attic login anon http://server:8080") + + # TODO: Make sure the correct status codes are returned + # (i.e., 500s shouldn't pass the "should fail" tests) + + with subtest("Check that we can create a cache"): + client.succeed("attic cache create test") + + with subtest("Check that we can push a path"): + client.succeed("cat ${testDrv} >test.nix && chmod +x test.nix") + test_file = client.succeed("nix-build --no-out-link test.nix") + test_file_hash = test_file.removeprefix("/nix/store/")[:32] + + client.succeed(f"attic push test {test_file}") + client.succeed(f"nix-store --delete {test_file}") + client.fail(f"grep hello {test_file}") + + with subtest("Check that we can pull a path"): + client.succeed("attic use readonly:test") + client.succeed(f"nix-store -r {test_file}") + client.succeed(f"grep hello {test_file}") + + with subtest("Check that we cannot push without required permissions"): + client.fail(f"attic push readonly:test {test_file}") + client.fail(f"attic push anon:test {test_file} 2>&1") + + with subtest("Check that we can make the cache public"): + client.fail("curl -sL --fail-with-body http://server:8080/test/nix-cache-info") + client.fail(f"curl -sL --fail-with-body http://server:8080/test/{test_file_hash}.narinfo") + client.succeed("attic cache configure test --public") + client.succeed("curl -sL --fail-with-body http://server:8080/test/nix-cache-info") + client.succeed(f"curl -sL --fail-with-body http://server:8080/test/{test_file_hash}.narinfo") + + with subtest("Check that we can trigger garbage collection"): + test_file_hash = test_file.removeprefix("/nix/store/")[:32] + client.succeed(f"curl -sL --fail-with-body http://server:8080/test/{test_file_hash}.narinfo") + client.succeed("attic cache configure test --retention-period 1s") + time.sleep(2) + server.succeed("${cmd.atticd} --mode garbage-collector-once") + client.fail(f"curl -sL --fail-with-body http://server:8080/test/{test_file_hash}.narinfo") + + with subtest("Check that we can destroy the cache"): + client.succeed("attic cache info test") + client.succeed("attic cache destroy --no-confirm test") + client.fail("attic cache info test") + client.fail("curl -sL --fail-with-body http://server:8080/test/nix-cache-info") + ''; + }; +} diff --git a/integration-tests/default.nix b/integration-tests/default.nix new file mode 100644 index 0000000..b5a1f4a --- /dev/null +++ b/integration-tests/default.nix @@ -0,0 +1,38 @@ +{ pkgs ? import ./nixpkgs.nix +, flake ? (import ../flake-compat.nix).defaultNix +}: + +let + inherit (pkgs) lib; + + nixosLib = import (pkgs.path + "/nixos/lib") { }; + + runTest = module: (nixosLib.evalTest ({ config, ... }: { + imports = [ + module + { + hostPkgs = pkgs; + _module.args.flake = flake; + } + ]; + result = config.test; + })).config.result; + + basicTests = let + matrix = { + database = [ "sqlite" "postgres" ]; + storage = [ "local" "minio" ]; + }; + in builtins.listToAttrs (map (e: { + name = "basic-${e.database}-${e.storage}"; + value = runTest { + imports = [ + ./basic + { + inherit (e) database storage; + } + ]; + }; + }) (lib.cartesianProductOfSets matrix)); +in { +} // basicTests diff --git a/integration-tests/nixpkgs.nix b/integration-tests/nixpkgs.nix new file mode 100644 index 0000000..e302996 --- /dev/null +++ b/integration-tests/nixpkgs.nix @@ -0,0 +1,7 @@ +let + flake = (import ../flake-compat.nix).defaultNix; +in import flake.inputs.nixpkgs.outPath { + overlays = [ + flake.overlays.default + ]; +}