diff --git a/.envrc b/.envrc index cffc922..a5dbbcb 100644 --- a/.envrc +++ b/.envrc @@ -1 +1 @@ -use flake . --impure +use flake . diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d16766..1f01344 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,3 +1,4 @@ +--- name: "Test" on: pull_request: @@ -28,24 +29,6 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Build run: nix develop --print-build-logs -v --command pre-commit run --all-files - # flake-checks: - # runs-on: ubuntu-latest - # strategy: - # matrix: - # check: [treefmt] - # needs: pre-job - # if: needs.pre-job.outputs.should_skip != 'true' - # steps: - # - uses: actions/checkout@v4 - # - uses: cachix/install-nix-action@v31 - # with: - # nix_path: nixpkgs=channel:nixos-unstable - # - uses: cachix/cachix-action@v16 - # with: - # name: wires - # authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - # - name: Build - # run: nix build .#checks.x86_64-linux.${{ matrix.check }} --print-build-logs nextest: runs-on: ubuntu-latest needs: pre-job @@ -68,3 +51,42 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Nextest run: nix develop --print-build-logs -v --command cargo nextest run + find-vm-tests: + runs-on: ubuntu-latest + needs: pre-job + if: needs.pre-job.outputs.should_skip != 'true' + outputs: + tests: ${{ steps.tests.outputs.tests }} + steps: + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@v16 + with: + name: wires + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + - name: find tests + id: tests + run: | + echo "tests=$( + nix eval --impure --json --expr \ + 'with builtins; filter ((import {}).lib.hasPrefix "nixos-vm-test") (attrNames (getFlake "${{ github.workspace }}").checks.x86_64-linux)' + )" >> "$GITHUB_OUTPUT" + vm-tests: + runs-on: self-hosted + needs: find-vm-tests + strategy: + matrix: + test: ${{ fromJSON(needs.find-vm-tests.outputs.tests) }} + steps: + - uses: actions/checkout@v4 + # - uses: cachix/install-nix-action@v31 + # with: + # nix_path: nixpkgs=channel:nixos-unstable + # - uses: cachix/cachix-action@v16 + # with: + # name: wires + # authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + - name: Build + run: nix build .#checks.x86_64-linux.${{ matrix.test }} --print-build-logs diff --git a/flake.nix b/flake.nix index 4a244c6..b87e407 100644 --- a/flake.nix +++ b/flake.nix @@ -12,6 +12,7 @@ }; outputs = { + self, flake-parts, systems, git-hooks, @@ -30,6 +31,7 @@ ./wire/cli ./wire/key_agent ./doc + ./tests/nix ]; systems = import systems; @@ -45,6 +47,7 @@ _module.args = { toolchain = inputs'.fenix.packages.complete; craneLib = (crane.mkLib pkgs).overrideToolchain config._module.args.toolchain.toolchain; + inherit self; }; treefmt = { programs = { diff --git a/intergration-testing/default.nix b/intergration-testing/default.nix deleted file mode 100644 index 6a9e423..0000000 --- a/intergration-testing/default.nix +++ /dev/null @@ -1,108 +0,0 @@ -{ - wire ? (import ../default.nix).flake.outputs.packages.x86_64-linux.wire, - pkgs ? (import ./nixpkgs.nix), - ... -}: -let - inherit (pkgs) lib; - sshKeys = import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs; - - commonModule = - { pkgs, ... }: - { - nix.nixPath = [ "nixpkgs=${pkgs.path}" ]; - nix.settings.substituters = lib.mkForce [ ]; - virtualisation = { - memorySize = lib.mkForce (1024 * 5); - writableStore = true; - additionalPaths = [ - pkgs.path - (getPrebuiltNode "node") - (import ../default.nix).tarball - (import ../default.nix).flake.inputs.nixpkgs.outPath - ./.. - ]; - }; - - services.openssh.enable = true; - users.users.root.openssh.authorizedKeys.keys = [ - sshKeys.snakeOilPublicKey - ]; - - boot.loader.grub.enable = false; - }; - - deployerModule = - { pkgs, ... }: - { - imports = [ commonModule ]; - environment.systemPackages = [ - wire - pkgs.git - (pkgs.writeShellScriptBin "run-copy-stderr" '' - exec "$@" 2>&1 - '') - ]; - }; - - targetModule = - { ... }: - { - imports = [ commonModule ]; - system.switch.enable = true; - }; - - nodes = { - deployer = deployerModule; - node = targetModule; - }; - - evalTest = - module: - pkgs.testers.runNixOSTest { - inherit nodes; - name = "deployer"; - - imports = [ - module - # commonModule - ]; - }; - - evaluate = import ../runtime/evaluate.nix; - getPrebuiltNode = - name: - (evaluate { - hive = import ./hive.nix; - path = ./.; - nixosConfigurations = { }; - nixpkgs = pkgs; - }).getTopLevel - name; -in -evalTest ( - { pkgs, ... }: - { - testScript = _: '' - start_all() - - deployer.succeed("nix-store -qR ${getPrebuiltNode "node"}") - node.succeed("nix-store -qR ${getPrebuiltNode "node"}") - deployer.succeed("nix-store -qR ${pkgs.path}") - node.succeed("nix-store -qR ${pkgs.path}") - deployer.succeed("ln -sf ${pkgs.path} /nixpkgs") - node.succeed("ln -sf ${pkgs.path} /nixpkgs") - - node.wait_for_unit("sshd.service") - - # Make deployer use ssh snake oil - deployer.succeed("mkdir -p /root/.ssh && touch /root/.ssh/id_rsa && chmod 0600 /root/.ssh/id_rsa && cat ${sshKeys.snakeOilPrivateKey} > /root/.ssh/id_rsa") - - deployer.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new node true", timeout=30) - - deployer.succeed("wire apply switch --no-progress -vv --no-keys --path ${../.}/intergration-testing/") - - # node.succeed("stat /etc/post-switch") - ''; - } -) diff --git a/intergration-testing/hive.nix b/intergration-testing/hive.nix deleted file mode 100644 index 670c3b3..0000000 --- a/intergration-testing/hive.nix +++ /dev/null @@ -1,47 +0,0 @@ -{ - meta.nixpkgs = import ./nixpkgs.nix; - - node = - { - pkgs, - lib, - ... - }: - let - sshKeys = import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs; - in - { - deployment.target.host = "node"; - deployment.buildOnTarget = false; - - nix.nixPath = [ "nixpkgs=/nixpkgs" ]; - nix.settings.substituters = lib.mkForce [ ]; - virtualisation = { - memorySize = lib.mkForce (1024 * 5); - writableStore = true; - additionalPaths = [ pkgs.path ]; - }; - - services.openssh.enable = true; - users.users.root.openssh.authorizedKeys.keys = [ - sshKeys.snakeOilPublicKey - ]; - - system.switch.enable = true; - - imports = - let - # WTF is this and why does it work? - pkgs = import ./nixpkgs.nix; - in - [ - (pkgs.path + "/nixos/lib/testing/nixos-test-base.nix") - ]; - - boot.loader.grub.enable = false; - - environment.etc."post-switch" = { - text = "exists"; - }; - }; -} diff --git a/intergration-testing/nixpkgs.nix b/intergration-testing/nixpkgs.nix deleted file mode 100644 index 02f63a0..0000000 --- a/intergration-testing/nixpkgs.nix +++ /dev/null @@ -1,4 +0,0 @@ -let - nixpkgs = (import ../default.nix).flake.inputs.nixpkgs.outPath; -in -import nixpkgs { } diff --git a/tests/nix/default.nix b/tests/nix/default.nix new file mode 100644 index 0000000..b6a313e --- /dev/null +++ b/tests/nix/default.nix @@ -0,0 +1,124 @@ +{ + self, + config, + lib, + inputs, + ... +}: +let + inherit (lib) + mkOption + mapAttrs' + mapAttrsToList + flatten + ; + inherit (lib.types) + submodule + lines + attrsOf + anything + lazyAttrsOf + ; + cfg = config.wire.testing; +in +{ + imports = [ ./suite/test_basic_deploy ]; + options.wire.testing = mkOption { + type = attrsOf ( + submodule ( + { name, ... }: + { + options = { + nodes = mkOption { + type = lazyAttrsOf anything; + }; + testScript = mkOption { + type = lines; + default = ''''; + description = "test script for runNixOSTest"; + }; + testDir = mkOption { + default = "${self}/tests/nix/suite/${name}"; + readOnly = true; + }; + }; + } + ) + ); + description = "A set of test cases for wire VM testing suite"; + }; + + config.perSystem = + { + pkgs, + self', + ... + }: + { + checks = mapAttrs' (testName: opts: rec { + name = "nixos-vm-test-${testName}"; + value = pkgs.testers.runNixOSTest { + inherit (opts) nodes; + name = testName; + defaults = + { + pkgs, + evaluateHive, + testDir, + ... + }: + let + + hive = evaluateHive { + nixpkgs = pkgs.path; + path = testDir; + hive = builtins.scopedImport { + __nixPath = _b: null; + __findFile = path: name: if name == "nixpkgs" then pkgs.path else throw "oops!!"; + } "${testDir}/hive.nix"; + }; + nodes = mapAttrsToList (_: val: val.config.system.build.toplevel.drvPath) hive.nodes; + # fetch **all** dependencies of a flake + # it's called fetchLayer because my naming skills are awful + fetchLayer = + input: + let + subLayers = if input ? inputs then map fetchLayer (builtins.attrValues input.inputs) else [ ]; + in + [ + input.outPath + ] + ++ subLayers; + in + { + imports = [ ./test-opts.nix ]; + nix = { + nixPath = [ "nixpkgs=${pkgs.path}" ]; + settings.substituters = lib.mkForce [ ]; + }; + + virtualisation.memorySize = 4096; + virtualisation.additionalPaths = flatten (nodes ++ (mapAttrsToList (_: fetchLayer) inputs)); + + }; + node.specialArgs = { + evaluateHive = import "${self}/runtime/evaluate.nix"; + inherit testName; + snakeOil = import "${pkgs.path}/nixos/tests/ssh-keys.nix" pkgs; + inherit (opts) testDir; + inherit (self'.packages) wire; + }; + # NOTE: there is surely a better way of doing this in a more + # "controlled" manner, but until a need is asked for, this will remain + # as is. + testScript = + '' + start_all() + '' + + lib.concatStringsSep "\n" (mapAttrsToList (_: value: value._wire.testScript) value.nodes) + + opts.testScript; + }; + + }) cfg; + }; +} diff --git a/tests/nix/suite/test_basic_deploy/default.nix b/tests/nix/suite/test_basic_deploy/default.nix new file mode 100644 index 0000000..0da0389 --- /dev/null +++ b/tests/nix/suite/test_basic_deploy/default.nix @@ -0,0 +1,15 @@ +{ config, ... }: +{ + wire.testing.test_basic_deploy = { + nodes.deployer = { + _wire.deployer = true; + }; + nodes.receiver = { + _wire.receiver = true; + }; + testScript = '' + deployer.succeed("wire apply --on receiver --no-progress --path ${config.wire.testing.test_basic_deploy.testDir}/hive.nix --no-keys -vvv >&2") + receiver.succeed("test -f /etc/a") + ''; + }; +} diff --git a/tests/nix/suite/test_basic_deploy/hive.nix b/tests/nix/suite/test_basic_deploy/hive.nix new file mode 100644 index 0000000..82077a6 --- /dev/null +++ b/tests/nix/suite/test_basic_deploy/hive.nix @@ -0,0 +1,9 @@ +let + mkHiveNode = import ../utils.nix { testName = "test_basic_deploy"; }; +in +{ + meta.nixpkgs = import { system = "x86_64-linux"; }; + receiver = mkHiveNode { hostname = "receiver"; } { + environment.etc."a".text = "b"; + }; +} diff --git a/tests/nix/suite/utils.nix b/tests/nix/suite/utils.nix new file mode 100644 index 0000000..184c321 --- /dev/null +++ b/tests/nix/suite/utils.nix @@ -0,0 +1,42 @@ +{ testName }: +let + # Use the flake-compat code in project root to access the tests which are + # defined through Flakes, as flake-parts is heavily depended on here. + flake = import ../../../.; +in +{ + + # This is glue for the newly deployed VMs as they need specific configuration + # such as static network configuration and other nitpicky VM-specific options. + # I thank Colmena & NixOps devs for providing me pointers on how to correctly create this, so + # thank you to those who made them! + # + mkHiveNode = + { + hostname, + system ? "x86_64-linux", + }: + cfg: { + imports = [ + cfg + ( + { + modulesPath, + ... + }: + { + imports = [ + "${modulesPath}/virtualisation/qemu-vm.nix" + "${modulesPath}/testing/test-instrumentation.nix" + flake.checks.${system}."nixos-vm-test-${testName}".nodes.${hostname}.system.build.networkConfig + ]; + + nixpkgs.hostPlatform = system; + boot.loader.grub.enable = false; + } + ) + ]; + }; + + __functor = self: self.mkHiveNode; +} diff --git a/tests/nix/test-opts.nix b/tests/nix/test-opts.nix new file mode 100644 index 0000000..0b499e0 --- /dev/null +++ b/tests/nix/test-opts.nix @@ -0,0 +1,52 @@ +{ + lib, + snakeOil, + wire, + config, + ... +}: +let + inherit (lib) + mkEnableOption + mkMerge + mkIf + mkOption + ; + inherit (lib.types) lines; + cfg = config._wire; +in +{ + options._wire = { + deployer = mkEnableOption "deployment-specific settings"; + receiver = mkEnableOption "receiver-specific settings"; + testScript = mkOption { + type = lines; + default = ""; + description = "node-specific test script"; + }; + }; + + config = mkMerge [ + (mkIf cfg.deployer { + systemd.tmpfiles.rules = [ + "C+ /root/.ssh/id_ed25519 600 - - - ${snakeOil.snakeOilEd25519PrivateKey}" + ]; + environment.systemPackages = [ wire ]; + # It's important to note that you should never ever use this configuration + # for production. You are risking a MITM attack with this! + programs.ssh.extraConfig = '' + Host * + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + ''; + + }) + (mkIf cfg.receiver { + services.openssh.enable = true; + users.users.root.openssh.authorizedKeys.keys = [ snakeOil.snakeOilEd25519PublicKey ]; + _wire.testScript = '' + ${config.networking.hostName}.wait_for_unit("sshd.service") + ''; + }) + ]; +}