diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7ff28dd..fb46d0d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -20,6 +20,13 @@ jobs: with: github_access_token: ${{ secrets.GITHUB_TOKEN }} + # ubuntu-latest advertises KVM (via /dev/kvm) so the NixOS VM tests can + # boot; the GitHub macOS runners cannot run Linux VMs, so the macOS job + # only runs the non-VM checks below. + - name: Advertise KVM and nixos-test build features + if: runner.os == 'Linux' + run: echo "system-features = kvm nixos-test" | sudo tee -a /etc/nix/nix.conf + - name: Enable binary cache with Cachix uses: cachix/cachix-action@v17 with: @@ -27,9 +34,18 @@ jobs: # If you chose API tokens for write access OR if you have a private cache authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - - name: Run Nix checks + - name: Run Nix checks (Ubuntu - including VM tests) + if: runner.os == 'Linux' run: nix flake check + - name: Run Nix checks (macOS - excluding VM tests) + if: runner.os == 'macOS' + run: | + nix build --no-link '.#checks.aarch64-darwin.eval-tests' \ + '.#checks.aarch64-darwin.smoke-nrdot-collector' \ + '.#checks.aarch64-darwin.smoke-nri-flex' \ + '.#checks.aarch64-darwin.format-check' + build: needs: [checks] runs-on: ${{ matrix.os }} diff --git a/checks/default.nix b/checks/default.nix new file mode 100644 index 0000000..6f192b7 --- /dev/null +++ b/checks/default.nix @@ -0,0 +1,62 @@ +# Aggregates all flake checks. +{ + lib, + pkgs, + inputs, +}: +let + # A pkgs set that also contains this flake's packages. We merge the plain + # packages into `pkgs` instead of using `pkgs.extend` with the overlay, since + # our package set no longer references stdenv at the top level (that would + # cause infinite recursion when an overlay is applied). + # + # newrelic-fluent-bit-output is Linux-only, so it is filtered out elsewhere. + ourPackages = import ../pkgs { pkgs = pkgs; }; + overlayPkgs = + pkgs + // ( + if pkgs.stdenv.hostPlatform.isLinux then + ourPackages + else + builtins.removeAttrs ourPackages [ "newrelic-fluent-bit-output" ] + ); + + # Package set for the NixOS VM guests. The test driver runs on the host + # (nixpkgs supports aarch64-darwin hosts driving aarch64-linux guests via + # HVF), but the guest itself is always Linux, so the flake's packages must + # be the Linux builds (`pkgs.pkgsLinux`). + guestPkgs = pkgs.pkgsLinux // import ../pkgs { pkgs = pkgs.pkgsLinux; }; +in +(import ./eval-tests.nix { + inherit lib; + pkgs = overlayPkgs; + plainPkgs = pkgs; +}) +// (import ./smoke-tests.nix { + inherit lib; + pkgs = overlayPkgs; +}) +// (import ./vm-tests.nix { + inherit lib; + pkgs = overlayPkgs; + inherit guestPkgs; +}) +// { + # Ensures every .nix file in the repo is formatted with the flake's formatter + format-check = + pkgs.runCommand "format-check" + { + files = builtins.filter (f: lib.hasSuffix ".nix" f) (lib.filesystem.listFilesRecursive ../.); + formatter = "${pkgs.nixfmt}/bin/nixfmt"; + } + '' + for f in $files; do + $formatter - <"$f" | cmp -s - "$f" || { + echo "NOT FORMATTED: $f" + echo "Run 'nix fmt' and commit the changes." + exit 1 + } + done + touch $out + ''; +} diff --git a/checks/eval-tests.nix b/checks/eval-tests.nix new file mode 100644 index 0000000..118a9bb --- /dev/null +++ b/checks/eval-tests.nix @@ -0,0 +1,264 @@ +# Unit tests that evaluate the modules and assert on the resulting config. +# These run quickly and don't build any packages. +{ + lib, + pkgs, + plainPkgs, +}: +let + inherit (lib) types mkOption runTests; + + # Minimal stub options so the NixOS modules can be evaluated standalone + moduleStubs = { + options = { + environment.etc = mkOption { + type = types.attrsOf types.anything; + default = { }; + }; + systemd.services = mkOption { + type = types.attrsOf types.anything; + default = { }; + }; + launchd.daemons = mkOption { + type = types.attrsOf types.anything; + default = { }; + }; + assertions = mkOption { + type = types.listOf ( + types.submodule { + options = { + assertion = mkOption { + type = types.bool; + }; + message = mkOption { + type = types.str; + }; + }; + } + ); + default = [ ]; + }; + }; + }; + + mkEval = + { + module, + pkg ? plainPkgs, + config, + }: + lib.evalModules { + modules = [ + moduleStubs + module + config + ]; + specialArgs = { + pkgs = pkg; + }; + }; + + infraModule = import ../modules/nixos/newrelic-infra.nix; + nrdotModule = import ../modules/nixos/nrdot-collector.nix; + + infraWith = + { + pkg ? plainPkgs, + config ? { }, + }: + (mkEval { + module = infraModule; + inherit pkg; + config = { + services.newrelic-infra = { + enable = true; + } + // config; + }; + }).config.services.newrelic-infra; + + infraConfigWith = + { + pkg ? plainPkgs, + config ? { }, + }: + (mkEval { + module = infraModule; + inherit pkg; + config = { + services.newrelic-infra = { + enable = true; + } + // config; + }; + }).config; + + # Only meaningful on Linux: the fluent-bit output plugin is Linux-only + linuxOnlyTests = lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { + testNixosInfraLoggingNrLibWithOverlay = { + expr = + (infraWith { + pkg = pkgs; + config = { + logging = [ + { + name = "test"; + file = "/var/log/test.log"; + } + ]; + }; + }).settings.fluent_bit_nr_lib_path; + expected = "${pkgs.newrelic-fluent-bit-output}/lib/out_newrelic.so"; + }; + + # The service runs with system utilities (kmod is Linux-only) on its PATH + testNixosInfraPathIncludesKmod = { + expr = lib.elem pkgs.kmod (infraConfigWith { }).systemd.services.newrelic-infra.path; + expected = true; + }; + }; + + tests = { + # The service is defined and uses the agent's service binary + testNixosInfraService = { + expr = (infraConfigWith { }).systemd.services.newrelic-infra.serviceConfig.ExecStart; + expected = "${pkgs.infrastructure-agent}/bin/newrelic-infra-service -config ${ + (pkgs.formats.yaml { }).generate "config.yaml" (infraWith { }).settings + }"; + }; + + # Regression test for https://github.com/DavSanchez/Nix-Relic/issues/88: + # the agent hardcodes FHS paths like /sbin/lsmod + testNixosInfraLsmodBindPaths = { + expr = (infraConfigWith { }).systemd.services.newrelic-infra.serviceConfig.BindReadOnlyPaths; + expected = [ + "/run/current-system/sw/bin:/sbin" + "/run/current-system/sw/bin/systemctl:/bin/systemctl" + ]; + }; + + # The package option defaults to nixpkgs' infrastructure-agent + testNixosInfraPackageDefault = { + expr = (infraWith { }).package; + expected = pkgs.infrastructure-agent; + }; + + # Log forwarding wires the fluent-bit executable into the agent settings + testNixosInfraLoggingFluentBitPath = { + expr = + (infraWith { + config = { + logging = [ + { + name = "test"; + file = "/var/log/test.log"; + } + ]; + }; + }).settings.fluent_bit_exe_path; + expected = "${pkgs.fluent-bit}/bin/fluent-bit"; + }; + + # Without the overlay there is no output plugin package + testNixosInfraLoggingNrLibWithoutOverlay = { + expr = + (infraWith { + config = { + logging = [ + { + name = "test"; + file = "/var/log/test.log"; + } + ]; + }; + }).fluentBitPackage; + expected = null; + }; + + # The logging config file is dropped into logging.d + testNixosInfraLoggingFileGenerated = { + expr = + (infraConfigWith { + config = { + logging = [ + { + name = "test"; + file = "/var/log/test.log"; + } + ]; + }; + }).environment.etc ? "newrelic-infra/logging.d/logging.yml"; + expected = true; + }; + + # No logging config file is generated when logging is disabled + testNixosInfraNoLoggingFileWithoutLogs = { + expr = (infraConfigWith { }).environment.etc ? "newrelic-infra/logging.d/logging.yml"; + expected = false; + }; + + # Integrations are written to integrations.d + testNixosInfraIntegrationsFileGenerated = { + expr = + (infraConfigWith { + config = { + integrations = [ + { + name = "flex"; + package = pkgs.nri-flex; + } + ]; + }; + }).environment.etc ? "newrelic-infra/integrations.d/flex.yml"; + expected = true; + }; + + # The nrdot-collector package is only available through the overlay + testNixosNrdotCollectorPackageFromOverlay = { + expr = + (mkEval { + module = nrdotModule; + pkg = pkgs; + config = { + services.nrdot-collector = { + enable = true; + settings = { }; + }; + }; + }).config.services.nrdot-collector.package; + expected = pkgs.nrdot-collector; + }; + + # The nrdot-collector service is defined with the expected binary + testNixosNrdotCollectorService = { + expr = + (mkEval { + module = nrdotModule; + pkg = pkgs; + config = { + services.nrdot-collector = { + enable = true; + settings = { }; + }; + }; + }).config.systemd.services.nrdot-collector.serviceConfig.ExecStart; + expected = "${pkgs.nrdot-collector}/bin/nrdot-collector --config=file:${ + (pkgs.formats.yaml { }).generate "config.yaml" { } + }"; + }; + }; + + failures = runTests (tests // linuxOnlyTests); +in +{ + eval-tests = pkgs.runCommand "eval-tests" { } ( + '' + ${lib.concatMapStringsSep "\n" (failure: '' + echo "FAIL: ${failure.name}" + echo " expected: ${lib.generators.toPretty { } failure.expected}" + echo " result: ${lib.generators.toPretty { } failure.result}" + '') failures} + '' + + (if failures == [ ] then "touch $out" else "exit 1") + ); +} diff --git a/checks/smoke-tests.nix b/checks/smoke-tests.nix new file mode 100644 index 0000000..19d748c --- /dev/null +++ b/checks/smoke-tests.nix @@ -0,0 +1,61 @@ +# Smoke tests that build each package and verify the binary behaves. +{ + lib, + pkgs, +}: +let + runTest = + name: text: + pkgs.testers.runCommand { + inherit name; + script = '' + echo "Running smoke test: ${name}" + bash -e <<'EOF' + ${text} + EOF + touch $out + ''; + }; + + nri-flex = pkgs.nri-flex; + nrdot-collector = pkgs.nrdot-collector; + plugin = pkgs.newrelic-fluent-bit-output or null; + + version = "2.0.0"; + configFile = pkgs.writeText "config.yaml" '' + receivers: + otlp: + protocols: + grpc: + endpoint: localhost:4317 + http: + endpoint: localhost:4318 + exporters: + debug: + verbosity: basic + extensions: + health_check: + service: + extensions: [health_check] + pipelines: + traces: + receivers: [otlp] + exporters: [debug] + ''; + smokeNrdot = runTest "smoke-nrdot-collector" '' + ${lib.getExe nrdot-collector} --version | grep -q "${version}" + ${lib.getExe nrdot-collector} validate --config=file:${configFile} + ''; +in +{ + smoke-nrdot-collector = smokeNrdot; + smoke-nri-flex = runTest "smoke-nri-flex" '' + ${lib.getExe nri-flex} -h >/dev/null 2>&1 + ''; +} +// lib.optionalAttrs (plugin != null) { + smoke-newrelic-fluent-bit-output = runTest "smoke-newrelic-fluent-bit-output" '' + test -f "${plugin}/lib/out_newrelic.so" + file "${plugin}/lib/out_newrelic.so" | grep -q "shared object" + ''; +} diff --git a/checks/vm-tests.nix b/checks/vm-tests.nix new file mode 100644 index 0000000..27063c1 --- /dev/null +++ b/checks/vm-tests.nix @@ -0,0 +1,127 @@ +# NixOS VM tests. The driver always runs on the host; nixpkgs supports +# aarch64-darwin hosts running aarch64-linux guests via HVF. +# +# Note: packages from this flake are injected explicitly through the modules' +# `package` options rather than via `nixpkgs.overlays`, because the NixOS test +# framework sets `nixpkgs.pkgs` and makes the `nixpkgs` configuration read-only. +{ + lib, + pkgs, + guestPkgs, +}: +let + # Outer pkgs (this flake's host package set), used for `testers.runNixOSTest`. + # The node definitions below shadow `pkgs` with the NixOS test framework's + # own package set, so the flake's packages injected into the guests must be + # the Linux builds (`guestPkgs`). + flakePkgs = pkgs; + + # nixpkgs pins gic-version=2 for aarch64-darwin hosts in qemu-common.nix, + # but qemu's HVF accelerator on Apple Silicon only supports GICv3, so guests + # never boot on darwin hosts without overriding the machine type. + # See https://github.com/NixOS/nixpkgs/issues/380524 + darwinQemuMachineOverride = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin [ + "-machine" + "virt,gic-version=3,accel=hvf" + ]; + + infraModule = import ../modules/nixos/newrelic-infra.nix; + nrdotModule = import ../modules/nixos/nrdot-collector.nix; +in +{ + # Boots a VM with the infra agent running and verifies the service comes up, + # the log-forwarding config is generated, and that the #88 fix (FHS /sbin + # paths bound into the service namespace) is in effect. + vm-newrelic-infra = flakePkgs.testers.runNixOSTest { + name = "newrelic-infra"; + nodes.machine = + { + pkgs, + lib, + ... + }: + { + imports = [ infraModule ]; + + services.newrelic-infra = { + enable = true; + package = guestPkgs.infrastructure-agent; + fluentBitPackage = guestPkgs.newrelic-fluent-bit-output; + settings.license_key = "test"; + logging = [ + { + name = "test-log"; + file = "/var/log/test.log"; + } + ]; + }; + + virtualisation.qemu.options = darwinQemuMachineOverride; + + environment.systemPackages = [ + pkgs.curl + pkgs.util-linux + ]; + }; + + testScript = '' + start_all() + machine.wait_for_unit("newrelic-infra.service") + machine.succeed("systemctl is-active newrelic-infra.service") + + # Regression test for #88: /sbin must be bound into the service's + # mount namespace so the agent can invoke /sbin/lsmod and friends. + # systemd resolves the /run/current-system/sw symlink, so the bind shows + # up as a dedicated /sbin mount (from the store's system-path). + pid = machine.succeed("systemctl show -p MainPID --value newrelic-infra.service").strip() + machine.succeed(f"grep -q ' /sbin ' /proc/{pid}/mountinfo") + machine.succeed(f"nsenter -t {pid} -m test -x /sbin/lsmod") + + # The log-forwarding config is written to logging.d + machine.succeed("test -f /etc/newrelic-infra/logging.d/logging.yml") + machine.succeed("grep -q 'test-log' /etc/newrelic-infra/logging.d/logging.yml") + ''; + }; + + # Boots a VM with the nrdot-collector running and verifies the service starts + # and its health check endpoint responds. + vm-nrdot-collector = flakePkgs.testers.runNixOSTest { + name = "nrdot-collector"; + nodes.machine = + { + pkgs, + lib, + ... + }: + { + imports = [ nrdotModule ]; + + services.nrdot-collector = { + enable = true; + package = guestPkgs.nrdot-collector; + settings = { + receivers.otlp.protocols.grpc.endpoint = "localhost:4317"; + receivers.otlp.protocols.http.endpoint = "localhost:4318"; + exporters.debug.verbosity = "basic"; + extensions.health_check = { }; + service.extensions = [ "health_check" ]; + service.pipelines.traces = { + receivers = [ "otlp" ]; + exporters = [ "debug" ]; + }; + }; + }; + + virtualisation.qemu.options = darwinQemuMachineOverride; + + environment.systemPackages = [ pkgs.curl ]; + }; + + testScript = '' + start_all() + machine.wait_for_unit("nrdot-collector.service") + machine.succeed("systemctl is-active nrdot-collector.service") + machine.wait_until_succeeds("curl -sf http://localhost:13133/") + ''; + }; +} diff --git a/flake.nix b/flake.nix index f405ba4..b44e1c6 100644 --- a/flake.nix +++ b/flake.nix @@ -31,6 +31,15 @@ builtins.removeAttrs ourPackages [ "newrelic-fluent-bit-output" ] ); + checks = forAllSystems ( + system: + import ./checks { + pkgs = nixpkgs.legacyPackages.${system}; + inherit (nixpkgs) lib; + inherit inputs; + } + ); + nixosModules = import ./modules/nixos; darwinModules = import ./modules/darwin;