ALPHA: wire is a tool to deploy nixos systems wire.althaea.zone/

init testing framework (#93)

Co-authored-by: marshmallow <marshycity@gmail.com>

authored by

lychee
marshmallow
and committed by
GitHub
924ec1a6 64593ab6

+286 -178
+1 -1
.envrc
··· 1 - use flake . --impure 1 + use flake .
+40 -18
.github/workflows/test.yml
··· 1 + --- 1 2 name: "Test" 2 3 on: 3 4 pull_request: ··· 28 29 authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 29 30 - name: Build 30 31 run: nix develop --print-build-logs -v --command pre-commit run --all-files 31 - # flake-checks: 32 - # runs-on: ubuntu-latest 33 - # strategy: 34 - # matrix: 35 - # check: [treefmt] 36 - # needs: pre-job 37 - # if: needs.pre-job.outputs.should_skip != 'true' 38 - # steps: 39 - # - uses: actions/checkout@v4 40 - # - uses: cachix/install-nix-action@v31 41 - # with: 42 - # nix_path: nixpkgs=channel:nixos-unstable 43 - # - uses: cachix/cachix-action@v16 44 - # with: 45 - # name: wires 46 - # authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 47 - # - name: Build 48 - # run: nix build .#checks.x86_64-linux.${{ matrix.check }} --print-build-logs 49 32 nextest: 50 33 runs-on: ubuntu-latest 51 34 needs: pre-job ··· 68 51 authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 69 52 - name: Nextest 70 53 run: nix develop --print-build-logs -v --command cargo nextest run 54 + find-vm-tests: 55 + runs-on: ubuntu-latest 56 + needs: pre-job 57 + if: needs.pre-job.outputs.should_skip != 'true' 58 + outputs: 59 + tests: ${{ steps.tests.outputs.tests }} 60 + steps: 61 + - uses: actions/checkout@v4 62 + - uses: cachix/install-nix-action@v31 63 + with: 64 + nix_path: nixpkgs=channel:nixos-unstable 65 + - uses: cachix/cachix-action@v16 66 + with: 67 + name: wires 68 + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 69 + - name: find tests 70 + id: tests 71 + run: | 72 + echo "tests=$( 73 + nix eval --impure --json --expr \ 74 + 'with builtins; filter ((import <nixpkgs>{}).lib.hasPrefix "nixos-vm-test") (attrNames (getFlake "${{ github.workspace }}").checks.x86_64-linux)' 75 + )" >> "$GITHUB_OUTPUT" 76 + vm-tests: 77 + runs-on: self-hosted 78 + needs: find-vm-tests 79 + strategy: 80 + matrix: 81 + test: ${{ fromJSON(needs.find-vm-tests.outputs.tests) }} 82 + steps: 83 + - uses: actions/checkout@v4 84 + # - uses: cachix/install-nix-action@v31 85 + # with: 86 + # nix_path: nixpkgs=channel:nixos-unstable 87 + # - uses: cachix/cachix-action@v16 88 + # with: 89 + # name: wires 90 + # authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 91 + - name: Build 92 + run: nix build .#checks.x86_64-linux.${{ matrix.test }} --print-build-logs
+3
flake.nix
··· 12 12 }; 13 13 outputs = 14 14 { 15 + self, 15 16 flake-parts, 16 17 systems, 17 18 git-hooks, ··· 30 31 ./wire/cli 31 32 ./wire/key_agent 32 33 ./doc 34 + ./tests/nix 33 35 ]; 34 36 systems = import systems; 35 37 ··· 45 47 _module.args = { 46 48 toolchain = inputs'.fenix.packages.complete; 47 49 craneLib = (crane.mkLib pkgs).overrideToolchain config._module.args.toolchain.toolchain; 50 + inherit self; 48 51 }; 49 52 treefmt = { 50 53 programs = {
-108
intergration-testing/default.nix
··· 1 - { 2 - wire ? (import ../default.nix).flake.outputs.packages.x86_64-linux.wire, 3 - pkgs ? (import ./nixpkgs.nix), 4 - ... 5 - }: 6 - let 7 - inherit (pkgs) lib; 8 - sshKeys = import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs; 9 - 10 - commonModule = 11 - { pkgs, ... }: 12 - { 13 - nix.nixPath = [ "nixpkgs=${pkgs.path}" ]; 14 - nix.settings.substituters = lib.mkForce [ ]; 15 - virtualisation = { 16 - memorySize = lib.mkForce (1024 * 5); 17 - writableStore = true; 18 - additionalPaths = [ 19 - pkgs.path 20 - (getPrebuiltNode "node") 21 - (import ../default.nix).tarball 22 - (import ../default.nix).flake.inputs.nixpkgs.outPath 23 - ./.. 24 - ]; 25 - }; 26 - 27 - services.openssh.enable = true; 28 - users.users.root.openssh.authorizedKeys.keys = [ 29 - sshKeys.snakeOilPublicKey 30 - ]; 31 - 32 - boot.loader.grub.enable = false; 33 - }; 34 - 35 - deployerModule = 36 - { pkgs, ... }: 37 - { 38 - imports = [ commonModule ]; 39 - environment.systemPackages = [ 40 - wire 41 - pkgs.git 42 - (pkgs.writeShellScriptBin "run-copy-stderr" '' 43 - exec "$@" 2>&1 44 - '') 45 - ]; 46 - }; 47 - 48 - targetModule = 49 - { ... }: 50 - { 51 - imports = [ commonModule ]; 52 - system.switch.enable = true; 53 - }; 54 - 55 - nodes = { 56 - deployer = deployerModule; 57 - node = targetModule; 58 - }; 59 - 60 - evalTest = 61 - module: 62 - pkgs.testers.runNixOSTest { 63 - inherit nodes; 64 - name = "deployer"; 65 - 66 - imports = [ 67 - module 68 - # commonModule 69 - ]; 70 - }; 71 - 72 - evaluate = import ../runtime/evaluate.nix; 73 - getPrebuiltNode = 74 - name: 75 - (evaluate { 76 - hive = import ./hive.nix; 77 - path = ./.; 78 - nixosConfigurations = { }; 79 - nixpkgs = pkgs; 80 - }).getTopLevel 81 - name; 82 - in 83 - evalTest ( 84 - { pkgs, ... }: 85 - { 86 - testScript = _: '' 87 - start_all() 88 - 89 - deployer.succeed("nix-store -qR ${getPrebuiltNode "node"}") 90 - node.succeed("nix-store -qR ${getPrebuiltNode "node"}") 91 - deployer.succeed("nix-store -qR ${pkgs.path}") 92 - node.succeed("nix-store -qR ${pkgs.path}") 93 - deployer.succeed("ln -sf ${pkgs.path} /nixpkgs") 94 - node.succeed("ln -sf ${pkgs.path} /nixpkgs") 95 - 96 - node.wait_for_unit("sshd.service") 97 - 98 - # Make deployer use ssh snake oil 99 - deployer.succeed("mkdir -p /root/.ssh && touch /root/.ssh/id_rsa && chmod 0600 /root/.ssh/id_rsa && cat ${sshKeys.snakeOilPrivateKey} > /root/.ssh/id_rsa") 100 - 101 - deployer.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new node true", timeout=30) 102 - 103 - deployer.succeed("wire apply switch --no-progress -vv --no-keys --path ${../.}/intergration-testing/") 104 - 105 - # node.succeed("stat /etc/post-switch") 106 - ''; 107 - } 108 - )
-47
intergration-testing/hive.nix
··· 1 - { 2 - meta.nixpkgs = import ./nixpkgs.nix; 3 - 4 - node = 5 - { 6 - pkgs, 7 - lib, 8 - ... 9 - }: 10 - let 11 - sshKeys = import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs; 12 - in 13 - { 14 - deployment.target.host = "node"; 15 - deployment.buildOnTarget = false; 16 - 17 - nix.nixPath = [ "nixpkgs=/nixpkgs" ]; 18 - nix.settings.substituters = lib.mkForce [ ]; 19 - virtualisation = { 20 - memorySize = lib.mkForce (1024 * 5); 21 - writableStore = true; 22 - additionalPaths = [ pkgs.path ]; 23 - }; 24 - 25 - services.openssh.enable = true; 26 - users.users.root.openssh.authorizedKeys.keys = [ 27 - sshKeys.snakeOilPublicKey 28 - ]; 29 - 30 - system.switch.enable = true; 31 - 32 - imports = 33 - let 34 - # WTF is this and why does it work? 35 - pkgs = import ./nixpkgs.nix; 36 - in 37 - [ 38 - (pkgs.path + "/nixos/lib/testing/nixos-test-base.nix") 39 - ]; 40 - 41 - boot.loader.grub.enable = false; 42 - 43 - environment.etc."post-switch" = { 44 - text = "exists"; 45 - }; 46 - }; 47 - }
-4
intergration-testing/nixpkgs.nix
··· 1 - let 2 - nixpkgs = (import ../default.nix).flake.inputs.nixpkgs.outPath; 3 - in 4 - import nixpkgs { }
+124
tests/nix/default.nix
··· 1 + { 2 + self, 3 + config, 4 + lib, 5 + inputs, 6 + ... 7 + }: 8 + let 9 + inherit (lib) 10 + mkOption 11 + mapAttrs' 12 + mapAttrsToList 13 + flatten 14 + ; 15 + inherit (lib.types) 16 + submodule 17 + lines 18 + attrsOf 19 + anything 20 + lazyAttrsOf 21 + ; 22 + cfg = config.wire.testing; 23 + in 24 + { 25 + imports = [ ./suite/test_basic_deploy ]; 26 + options.wire.testing = mkOption { 27 + type = attrsOf ( 28 + submodule ( 29 + { name, ... }: 30 + { 31 + options = { 32 + nodes = mkOption { 33 + type = lazyAttrsOf anything; 34 + }; 35 + testScript = mkOption { 36 + type = lines; 37 + default = ''''; 38 + description = "test script for runNixOSTest"; 39 + }; 40 + testDir = mkOption { 41 + default = "${self}/tests/nix/suite/${name}"; 42 + readOnly = true; 43 + }; 44 + }; 45 + } 46 + ) 47 + ); 48 + description = "A set of test cases for wire VM testing suite"; 49 + }; 50 + 51 + config.perSystem = 52 + { 53 + pkgs, 54 + self', 55 + ... 56 + }: 57 + { 58 + checks = mapAttrs' (testName: opts: rec { 59 + name = "nixos-vm-test-${testName}"; 60 + value = pkgs.testers.runNixOSTest { 61 + inherit (opts) nodes; 62 + name = testName; 63 + defaults = 64 + { 65 + pkgs, 66 + evaluateHive, 67 + testDir, 68 + ... 69 + }: 70 + let 71 + 72 + hive = evaluateHive { 73 + nixpkgs = pkgs.path; 74 + path = testDir; 75 + hive = builtins.scopedImport { 76 + __nixPath = _b: null; 77 + __findFile = path: name: if name == "nixpkgs" then pkgs.path else throw "oops!!"; 78 + } "${testDir}/hive.nix"; 79 + }; 80 + nodes = mapAttrsToList (_: val: val.config.system.build.toplevel.drvPath) hive.nodes; 81 + # fetch **all** dependencies of a flake 82 + # it's called fetchLayer because my naming skills are awful 83 + fetchLayer = 84 + input: 85 + let 86 + subLayers = if input ? inputs then map fetchLayer (builtins.attrValues input.inputs) else [ ]; 87 + in 88 + [ 89 + input.outPath 90 + ] 91 + ++ subLayers; 92 + in 93 + { 94 + imports = [ ./test-opts.nix ]; 95 + nix = { 96 + nixPath = [ "nixpkgs=${pkgs.path}" ]; 97 + settings.substituters = lib.mkForce [ ]; 98 + }; 99 + 100 + virtualisation.memorySize = 4096; 101 + virtualisation.additionalPaths = flatten (nodes ++ (mapAttrsToList (_: fetchLayer) inputs)); 102 + 103 + }; 104 + node.specialArgs = { 105 + evaluateHive = import "${self}/runtime/evaluate.nix"; 106 + inherit testName; 107 + snakeOil = import "${pkgs.path}/nixos/tests/ssh-keys.nix" pkgs; 108 + inherit (opts) testDir; 109 + inherit (self'.packages) wire; 110 + }; 111 + # NOTE: there is surely a better way of doing this in a more 112 + # "controlled" manner, but until a need is asked for, this will remain 113 + # as is. 114 + testScript = 115 + '' 116 + start_all() 117 + '' 118 + + lib.concatStringsSep "\n" (mapAttrsToList (_: value: value._wire.testScript) value.nodes) 119 + + opts.testScript; 120 + }; 121 + 122 + }) cfg; 123 + }; 124 + }
+15
tests/nix/suite/test_basic_deploy/default.nix
··· 1 + { config, ... }: 2 + { 3 + wire.testing.test_basic_deploy = { 4 + nodes.deployer = { 5 + _wire.deployer = true; 6 + }; 7 + nodes.receiver = { 8 + _wire.receiver = true; 9 + }; 10 + testScript = '' 11 + deployer.succeed("wire apply --on receiver --no-progress --path ${config.wire.testing.test_basic_deploy.testDir}/hive.nix --no-keys -vvv >&2") 12 + receiver.succeed("test -f /etc/a") 13 + ''; 14 + }; 15 + }
+9
tests/nix/suite/test_basic_deploy/hive.nix
··· 1 + let 2 + mkHiveNode = import ../utils.nix { testName = "test_basic_deploy"; }; 3 + in 4 + { 5 + meta.nixpkgs = import <nixpkgs> { system = "x86_64-linux"; }; 6 + receiver = mkHiveNode { hostname = "receiver"; } { 7 + environment.etc."a".text = "b"; 8 + }; 9 + }
+42
tests/nix/suite/utils.nix
··· 1 + { testName }: 2 + let 3 + # Use the flake-compat code in project root to access the tests which are 4 + # defined through Flakes, as flake-parts is heavily depended on here. 5 + flake = import ../../../.; 6 + in 7 + { 8 + 9 + # This is glue for the newly deployed VMs as they need specific configuration 10 + # such as static network configuration and other nitpicky VM-specific options. 11 + # I thank Colmena & NixOps devs for providing me pointers on how to correctly create this, so 12 + # thank you to those who made them! 13 + # 14 + mkHiveNode = 15 + { 16 + hostname, 17 + system ? "x86_64-linux", 18 + }: 19 + cfg: { 20 + imports = [ 21 + cfg 22 + ( 23 + { 24 + modulesPath, 25 + ... 26 + }: 27 + { 28 + imports = [ 29 + "${modulesPath}/virtualisation/qemu-vm.nix" 30 + "${modulesPath}/testing/test-instrumentation.nix" 31 + flake.checks.${system}."nixos-vm-test-${testName}".nodes.${hostname}.system.build.networkConfig 32 + ]; 33 + 34 + nixpkgs.hostPlatform = system; 35 + boot.loader.grub.enable = false; 36 + } 37 + ) 38 + ]; 39 + }; 40 + 41 + __functor = self: self.mkHiveNode; 42 + }
+52
tests/nix/test-opts.nix
··· 1 + { 2 + lib, 3 + snakeOil, 4 + wire, 5 + config, 6 + ... 7 + }: 8 + let 9 + inherit (lib) 10 + mkEnableOption 11 + mkMerge 12 + mkIf 13 + mkOption 14 + ; 15 + inherit (lib.types) lines; 16 + cfg = config._wire; 17 + in 18 + { 19 + options._wire = { 20 + deployer = mkEnableOption "deployment-specific settings"; 21 + receiver = mkEnableOption "receiver-specific settings"; 22 + testScript = mkOption { 23 + type = lines; 24 + default = ""; 25 + description = "node-specific test script"; 26 + }; 27 + }; 28 + 29 + config = mkMerge [ 30 + (mkIf cfg.deployer { 31 + systemd.tmpfiles.rules = [ 32 + "C+ /root/.ssh/id_ed25519 600 - - - ${snakeOil.snakeOilEd25519PrivateKey}" 33 + ]; 34 + environment.systemPackages = [ wire ]; 35 + # It's important to note that you should never ever use this configuration 36 + # for production. You are risking a MITM attack with this! 37 + programs.ssh.extraConfig = '' 38 + Host * 39 + StrictHostKeyChecking no 40 + UserKnownHostsFile /dev/null 41 + ''; 42 + 43 + }) 44 + (mkIf cfg.receiver { 45 + services.openssh.enable = true; 46 + users.users.root.openssh.authorizedKeys.keys = [ snakeOil.snakeOilEd25519PublicKey ]; 47 + _wire.testScript = '' 48 + ${config.networking.hostName}.wait_for_unit("sshd.service") 49 + ''; 50 + }) 51 + ]; 52 + }