nix: cleanup
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ia88656a1d6bb152398a5c4ce83d40a3e6a6a6964
This commit is contained in:
parent
794e4a8e61
commit
62f8cdf4de
13 changed files with 514 additions and 529 deletions
34
flake.nix
34
flake.nix
|
|
@ -46,12 +46,7 @@
|
||||||
|
|
||||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||||
in {
|
in {
|
||||||
demo-vm = pkgs.callPackage ./nix/demo-vm.nix {
|
demo-vm = pkgs.callPackage ./nix/demo-vm.nix {inherit self;};
|
||||||
nixosModule = self.nixosModules.default;
|
|
||||||
fc-packages = {
|
|
||||||
inherit (self.packages.${system}) fc-common fc-evaluator fc-migrate-cli fc-queue-runner fc-server;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# FC Packages
|
# FC Packages
|
||||||
fc-common = pkgs.callPackage ./nix/packages/fc-common.nix {
|
fc-common = pkgs.callPackage ./nix/packages/fc-common.nix {
|
||||||
|
|
@ -77,35 +72,26 @@
|
||||||
|
|
||||||
checks = forAllSystems (system: let
|
checks = forAllSystems (system: let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
testArgs = {
|
|
||||||
nixosModule = self.nixosModules.default;
|
|
||||||
fc-packages = {
|
|
||||||
inherit (self.packages.${system}) fc-common fc-evaluator fc-migrate-cli fc-queue-runner fc-server;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in {
|
in {
|
||||||
# Split VM integration tests
|
# Split VM integration tests
|
||||||
service-startup = pkgs.callPackage ./nix/tests/service-startup.nix testArgs;
|
service-startup = pkgs.callPackage ./nix/tests/startup.nix {inherit self;};
|
||||||
basic-api = pkgs.callPackage ./nix/tests/basic-api.nix testArgs;
|
basic-api = pkgs.callPackage ./nix/tests/basic-api.nix {inherit self;};
|
||||||
auth-rbac = pkgs.callPackage ./nix/tests/auth-rbac.nix testArgs;
|
auth-rbac = pkgs.callPackage ./nix/tests/auth-rbac.nix {inherit self;};
|
||||||
api-crud = pkgs.callPackage ./nix/tests/api-crud.nix testArgs;
|
api-crud = pkgs.callPackage ./nix/tests/api-crud.nix {inherit self;};
|
||||||
features = pkgs.callPackage ./nix/tests/features.nix testArgs;
|
features = pkgs.callPackage ./nix/tests/features.nix {inherit self;};
|
||||||
e2e = pkgs.callPackage ./nix/tests/e2e.nix testArgs;
|
webhooks = pkgs.callPackage ./nix/tests/webhooks.nix {inherit self;};
|
||||||
|
e2e = pkgs.callPackage ./nix/tests/e2e.nix {inherit self;};
|
||||||
# Legacy monolithic test (for reference, can be removed after split tests pass)
|
|
||||||
vm-test = pkgs.callPackage ./nix/vm-test.nix testArgs;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
devShells = forAllSystems (system: let
|
devShells = forAllSystems (system: let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
in {
|
in {
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
name = "fc";
|
name = "fc-dev";
|
||||||
inputsFrom = [self.packages.${system}.fc-server];
|
inputsFrom = [self.packages.${system}.fc-server];
|
||||||
|
|
||||||
strictDeps = true;
|
strictDeps = true;
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
postgresql
|
|
||||||
pkg-config
|
pkg-config
|
||||||
openssl
|
openssl
|
||||||
|
|
||||||
|
|
@ -116,5 +102,7 @@
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.alejandra);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
{
|
{
|
||||||
pkgs,
|
pkgs,
|
||||||
fc-packages,
|
self,
|
||||||
nixosModule,
|
|
||||||
}: let
|
}: let
|
||||||
|
fc-packages = self.packages.${pkgs.stdenv.hostPlatform.system};
|
||||||
nixos = pkgs.nixos ({
|
nixos = pkgs.nixos ({
|
||||||
modulesPath,
|
modulesPath,
|
||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
}: {
|
}: {
|
||||||
imports = [
|
imports = [
|
||||||
nixosModule
|
self.nixosModules.fc-ci
|
||||||
(modulesPath + "/virtualisation/qemu-vm.nix")
|
(modulesPath + "/virtualisation/qemu-vm.nix")
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
evaluatorPackage = fc-packages.fc-evaluator;
|
evaluatorPackage = fc-packages.fc-evaluator;
|
||||||
queueRunnerPackage = fc-packages.fc-queue-runner;
|
queueRunnerPackage = fc-packages.fc-queue-runner;
|
||||||
migratePackage = fc-packages.fc-migrate-cli;
|
migratePackage = fc-packages.fc-migrate-cli;
|
||||||
|
|
||||||
server.enable = true;
|
server.enable = true;
|
||||||
evaluator.enable = true;
|
evaluator.enable = true;
|
||||||
queueRunner.enable = true;
|
queueRunner.enable = true;
|
||||||
|
|
|
||||||
|
|
@ -4,60 +4,73 @@
|
||||||
lib,
|
lib,
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
|
inherit (lib.modules) mkIf mkDefault;
|
||||||
inherit (lib.options) mkOption mkEnableOption;
|
inherit (lib.options) mkOption mkEnableOption;
|
||||||
inherit (lib.types) bool str int package listOf submodule nullOr;
|
inherit (lib.types) bool str int package listOf submodule nullOr;
|
||||||
|
inherit (lib.attrsets) recursiveUpdate optionalAttrs;
|
||||||
|
inherit (lib.lists) optional map;
|
||||||
|
|
||||||
cfg = config.services.fc;
|
cfg = config.services.fc;
|
||||||
|
|
||||||
settingsFormat = pkgs.formats.toml {};
|
settingsFormat = pkgs.formats.toml {};
|
||||||
settingsType = settingsFormat.type;
|
settingsType = settingsFormat.type;
|
||||||
|
|
||||||
# Build the final settings by merging declarative config into settings
|
# Build the final settings by merging declarative config into settings
|
||||||
finalSettings = lib.recursiveUpdate cfg.settings (lib.optionalAttrs (cfg.declarative.projects != [] || cfg.declarative.apiKeys != []) {
|
finalSettings = recursiveUpdate cfg.settings (optionalAttrs (cfg.declarative.projects != [] || cfg.declarative.apiKeys != []) {
|
||||||
declarative = {
|
declarative = {
|
||||||
projects = map (p: {
|
projects =
|
||||||
name = p.name;
|
map (p: {
|
||||||
repository_url = p.repositoryUrl;
|
name = p.name;
|
||||||
description = p.description or null;
|
repository_url = p.repositoryUrl;
|
||||||
jobsets = map (j: {
|
description = p.description or null;
|
||||||
name = j.name;
|
jobsets =
|
||||||
nix_expression = j.nixExpression;
|
map (j: {
|
||||||
enabled = j.enabled;
|
name = j.name;
|
||||||
flake_mode = j.flakeMode;
|
nix_expression = j.nixExpression;
|
||||||
check_interval = j.checkInterval;
|
enabled = j.enabled;
|
||||||
}) p.jobsets;
|
flake_mode = j.flakeMode;
|
||||||
}) cfg.declarative.projects;
|
check_interval = j.checkInterval;
|
||||||
api_keys = map (k: {
|
})
|
||||||
name = k.name;
|
p.jobsets;
|
||||||
key = k.key;
|
})
|
||||||
role = k.role;
|
cfg.declarative.projects;
|
||||||
}) cfg.declarative.apiKeys;
|
|
||||||
|
api_keys =
|
||||||
|
map (k: {
|
||||||
|
name = k.name;
|
||||||
|
key = k.key;
|
||||||
|
role = k.role;
|
||||||
|
})
|
||||||
|
cfg.declarative.apiKeys;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsFile = settingsFormat.generate "fc.toml" finalSettings;
|
settingsFile = settingsFormat.generate "fc.toml" finalSettings;
|
||||||
|
|
||||||
inherit (builtins) map;
|
|
||||||
|
|
||||||
jobsetOpts = {
|
jobsetOpts = {
|
||||||
options = {
|
options = {
|
||||||
name = mkOption {
|
|
||||||
type = str;
|
|
||||||
description = "Jobset name.";
|
|
||||||
};
|
|
||||||
nixExpression = mkOption {
|
|
||||||
type = str;
|
|
||||||
description = "Nix expression to evaluate (e.g. 'packages', 'checks', 'hydraJobs').";
|
|
||||||
};
|
|
||||||
enabled = mkOption {
|
enabled = mkOption {
|
||||||
type = bool;
|
type = bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = "Whether this jobset is enabled for evaluation.";
|
description = "Whether this jobset is enabled for evaluation.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
name = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Jobset name.";
|
||||||
|
};
|
||||||
|
|
||||||
|
nixExpression = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Nix expression to evaluate (e.g. 'packages', 'checks', 'hydraJobs').";
|
||||||
|
};
|
||||||
|
|
||||||
flakeMode = mkOption {
|
flakeMode = mkOption {
|
||||||
type = bool;
|
type = bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = "Whether to evaluate as a flake.";
|
description = "Whether to evaluate as a flake.";
|
||||||
};
|
};
|
||||||
|
|
||||||
checkInterval = mkOption {
|
checkInterval = mkOption {
|
||||||
type = int;
|
type = int;
|
||||||
default = 60;
|
default = 60;
|
||||||
|
|
@ -72,15 +85,18 @@
|
||||||
type = str;
|
type = str;
|
||||||
description = "Project name (unique identifier).";
|
description = "Project name (unique identifier).";
|
||||||
};
|
};
|
||||||
|
|
||||||
repositoryUrl = mkOption {
|
repositoryUrl = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Git repository URL.";
|
description = "Git repository URL.";
|
||||||
};
|
};
|
||||||
|
|
||||||
description = mkOption {
|
description = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
default = null;
|
default = null;
|
||||||
description = "Optional project description.";
|
description = "Optional project description.";
|
||||||
};
|
};
|
||||||
|
|
||||||
jobsets = mkOption {
|
jobsets = mkOption {
|
||||||
type = listOf (submodule jobsetOpts);
|
type = listOf (submodule jobsetOpts);
|
||||||
default = [];
|
default = [];
|
||||||
|
|
@ -95,6 +111,7 @@
|
||||||
type = str;
|
type = str;
|
||||||
description = "Human-readable name for this API key.";
|
description = "Human-readable name for this API key.";
|
||||||
};
|
};
|
||||||
|
|
||||||
key = mkOption {
|
key = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = ''
|
description = ''
|
||||||
|
|
@ -102,10 +119,21 @@
|
||||||
Will be hashed before storage. Consider using a secrets manager.
|
Will be hashed before storage. Consider using a secrets manager.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
role = mkOption {
|
role = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
default = "admin";
|
default = "admin";
|
||||||
description = "Role: admin, read-only, create-projects, eval-jobset, cancel-build, restart-jobs, bump-to-front.";
|
description = ''
|
||||||
|
Role:
|
||||||
|
|
||||||
|
* admin,
|
||||||
|
* read-only,
|
||||||
|
* create-projects,
|
||||||
|
* eval-jobset,
|
||||||
|
* cancel-build,
|
||||||
|
* restart-jobs,
|
||||||
|
* bump-to-front.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -121,13 +149,15 @@ in {
|
||||||
evaluatorPackage = mkOption {
|
evaluatorPackage = mkOption {
|
||||||
type = package;
|
type = package;
|
||||||
default = cfg.package;
|
default = cfg.package;
|
||||||
description = "The FC evaluator package. Defaults to cfg.package.";
|
defaultText = "cfg.package";
|
||||||
|
description = "The FC evaluator package.";
|
||||||
};
|
};
|
||||||
|
|
||||||
queueRunnerPackage = mkOption {
|
queueRunnerPackage = mkOption {
|
||||||
type = package;
|
type = package;
|
||||||
default = cfg.package;
|
default = cfg.package;
|
||||||
description = "The FC queue runner package. Defaults to cfg.package.";
|
defaultText = "cfg.package";
|
||||||
|
description = "The FC queue runner package.";
|
||||||
};
|
};
|
||||||
|
|
||||||
migratePackage = mkOption {
|
migratePackage = mkOption {
|
||||||
|
|
@ -139,8 +169,8 @@ in {
|
||||||
type = settingsType;
|
type = settingsType;
|
||||||
default = {};
|
default = {};
|
||||||
description = ''
|
description = ''
|
||||||
FC configuration as a Nix attribute set.
|
FC configuration as a Nix attribute set. Will be converted to TOML
|
||||||
Will be converted to TOML and written to fc.toml.
|
and written to {file}`fc.toml`.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -152,19 +182,23 @@ in {
|
||||||
Declarative project definitions. These are upserted on every
|
Declarative project definitions. These are upserted on every
|
||||||
server startup, ensuring the database matches this configuration.
|
server startup, ensuring the database matches this configuration.
|
||||||
'';
|
'';
|
||||||
example = lib.literalExpression ''
|
example = [
|
||||||
[
|
{
|
||||||
{
|
name = "my-project";
|
||||||
name = "my-project";
|
repositoryUrl = "https://github.com/user/repo";
|
||||||
repositoryUrl = "https://github.com/user/repo";
|
description = "My Nix project";
|
||||||
description = "My Nix project";
|
jobsets = [
|
||||||
jobsets = [
|
{
|
||||||
{ name = "packages"; nixExpression = "packages"; }
|
name = "packages";
|
||||||
{ name = "checks"; nixExpression = "checks"; }
|
nixExpression = "packages";
|
||||||
];
|
}
|
||||||
}
|
{
|
||||||
]
|
name = "checks";
|
||||||
'';
|
nixExpression = "checks";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
apiKeys = mkOption {
|
apiKeys = mkOption {
|
||||||
|
|
@ -174,12 +208,18 @@ in {
|
||||||
Declarative API key definitions. Keys are upserted on every
|
Declarative API key definitions. Keys are upserted on every
|
||||||
server startup. Use a secrets manager for production deployments.
|
server startup. Use a secrets manager for production deployments.
|
||||||
'';
|
'';
|
||||||
example = lib.literalExpression ''
|
example = [
|
||||||
[
|
{
|
||||||
{ name = "admin"; key = "fc_admin_secret"; role = "admin"; }
|
name = "admin";
|
||||||
{ name = "ci-bot"; key = "fc_ci_bot_key"; role = "eval-jobset"; }
|
key = "fc_admin_secret";
|
||||||
]
|
role = "admin";
|
||||||
'';
|
}
|
||||||
|
{
|
||||||
|
name = "ci-bot";
|
||||||
|
key = "fc_ci_bot_key";
|
||||||
|
role = "eval-jobset";
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -204,7 +244,7 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
users.users.fc = {
|
users.users.fc = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = "fc";
|
group = "fc";
|
||||||
|
|
@ -214,7 +254,7 @@ in {
|
||||||
|
|
||||||
users.groups.fc = {};
|
users.groups.fc = {};
|
||||||
|
|
||||||
services.postgresql = lib.mkIf cfg.database.createLocally {
|
services.postgresql = mkIf cfg.database.createLocally {
|
||||||
enable = true;
|
enable = true;
|
||||||
ensureDatabases = ["fc"];
|
ensureDatabases = ["fc"];
|
||||||
ensureUsers = [
|
ensureUsers = [
|
||||||
|
|
@ -225,14 +265,18 @@ in {
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
services.fc.settings = lib.mkDefault {
|
services.fc.settings = mkDefault {
|
||||||
database.url = "postgresql:///fc?host=/run/postgresql";
|
database.url = "postgresql:///fc?host=/run/postgresql";
|
||||||
server.host = "127.0.0.1";
|
server.host = "127.0.0.1";
|
||||||
server.port = 3000;
|
server.port = 3000;
|
||||||
gc.gc_roots_dir = "/nix/var/nix/gcroots/per-user/fc/fc-roots";
|
|
||||||
gc.enabled = true;
|
gc = {
|
||||||
gc.max_age_days = 30;
|
gc_roots_dir = "/nix/var/nix/gcroots/per-user/fc/fc-roots";
|
||||||
gc.cleanup_interval = 3600;
|
enabled = true;
|
||||||
|
max_age_days = 30;
|
||||||
|
cleanup_interval = 3600;
|
||||||
|
};
|
||||||
|
|
||||||
logs.log_dir = "/var/lib/fc/logs";
|
logs.log_dir = "/var/lib/fc/logs";
|
||||||
cache.enabled = true;
|
cache.enabled = true;
|
||||||
evaluator.restrict_eval = true;
|
evaluator.restrict_eval = true;
|
||||||
|
|
@ -240,125 +284,129 @@ in {
|
||||||
signing.enabled = false;
|
signing.enabled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd = {
|
||||||
(lib.mkIf cfg.server.enable "d /var/lib/fc/logs 0750 fc fc -")
|
tmpfiles.rules = [
|
||||||
(lib.mkIf cfg.queueRunner.enable "d /nix/var/nix/gcroots/per-user/fc 0755 fc fc -")
|
(mkIf cfg.server.enable "d /var/lib/fc/logs 0750 fc fc -")
|
||||||
];
|
(mkIf cfg.queueRunner.enable "d /nix/var/nix/gcroots/per-user/fc 0755 fc fc -")
|
||||||
|
|
||||||
systemd.services.fc-server = lib.mkIf cfg.server.enable {
|
|
||||||
description = "FC CI Server";
|
|
||||||
wantedBy = ["multi-user.target"];
|
|
||||||
after = ["network.target"] ++ lib.optional cfg.database.createLocally "postgresql.target";
|
|
||||||
requires = lib.optional cfg.database.createLocally "postgresql.target";
|
|
||||||
|
|
||||||
path = with pkgs; [nix zstd];
|
|
||||||
|
|
||||||
serviceConfig = {
|
|
||||||
ExecStartPre = "${cfg.migratePackage}/bin/fc-migrate up ${finalSettings.database.url or "postgresql:///fc?host=/run/postgresql"}";
|
|
||||||
ExecStart = "${cfg.package}/bin/fc-server";
|
|
||||||
Restart = "on-failure";
|
|
||||||
RestartSec = 5;
|
|
||||||
User = "fc";
|
|
||||||
Group = "fc";
|
|
||||||
StateDirectory = "fc";
|
|
||||||
LogsDirectory = "fc";
|
|
||||||
WorkingDirectory = "/var/lib/fc";
|
|
||||||
ReadWritePaths = ["/var/lib/fc"];
|
|
||||||
|
|
||||||
# Hardening
|
|
||||||
ProtectSystem = "strict";
|
|
||||||
ProtectHome = true;
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
environment = {
|
|
||||||
FC_CONFIG_FILE = "${settingsFile}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.fc-evaluator = lib.mkIf cfg.evaluator.enable {
|
|
||||||
description = "FC CI Evaluator";
|
|
||||||
wantedBy = ["multi-user.target"];
|
|
||||||
after = ["network.target" "fc-server.service"] ++ lib.optional cfg.database.createLocally "postgresql.target";
|
|
||||||
requires = ["fc-server.service"] ++ lib.optional cfg.database.createLocally "postgresql.target";
|
|
||||||
|
|
||||||
path = with pkgs; [
|
|
||||||
nix
|
|
||||||
git
|
|
||||||
nix-eval-jobs
|
|
||||||
];
|
];
|
||||||
|
|
||||||
serviceConfig = {
|
services = {
|
||||||
ExecStart = "${cfg.evaluatorPackage}/bin/fc-evaluator";
|
fc-server = mkIf cfg.server.enable {
|
||||||
Restart = "on-failure";
|
description = "FC CI Server";
|
||||||
RestartSec = 10;
|
wantedBy = ["multi-user.target"];
|
||||||
User = "fc";
|
after = ["network.target"] ++ optional cfg.database.createLocally "postgresql.target";
|
||||||
Group = "fc";
|
requires = optional cfg.database.createLocally "postgresql.target";
|
||||||
StateDirectory = "fc";
|
|
||||||
WorkingDirectory = "/var/lib/fc";
|
|
||||||
ReadWritePaths = ["/var/lib/fc"];
|
|
||||||
|
|
||||||
# Hardening
|
path = with pkgs; [nix zstd];
|
||||||
ProtectSystem = "strict";
|
|
||||||
ProtectHome = true;
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
environment = {
|
serviceConfig = {
|
||||||
FC_CONFIG_FILE = "${settingsFile}";
|
ExecStartPre = "${cfg.migratePackage}/bin/fc-migrate up ${finalSettings.database.url or "postgresql:///fc?host=/run/postgresql"}";
|
||||||
FC_EVALUATOR__WORK_DIR = "/var/lib/fc/evaluator";
|
ExecStart = "${cfg.package}/bin/fc-server";
|
||||||
FC_EVALUATOR__RESTRICT_EVAL = "true";
|
Restart = "on-failure";
|
||||||
};
|
RestartSec = 5;
|
||||||
};
|
User = "fc";
|
||||||
|
Group = "fc";
|
||||||
|
StateDirectory = "fc";
|
||||||
|
LogsDirectory = "fc";
|
||||||
|
WorkingDirectory = "/var/lib/fc";
|
||||||
|
ReadWritePaths = ["/var/lib/fc"];
|
||||||
|
|
||||||
systemd.services.fc-queue-runner = lib.mkIf cfg.queueRunner.enable {
|
# Hardening
|
||||||
description = "FC CI Queue Runner";
|
ProtectSystem = "strict";
|
||||||
wantedBy = ["multi-user.target"];
|
ProtectHome = true;
|
||||||
after = ["network.target" "fc-server.service"] ++ lib.optional cfg.database.createLocally "postgresql.target";
|
NoNewPrivileges = true;
|
||||||
requires = ["fc-server.service"] ++ lib.optional cfg.database.createLocally "postgresql.target";
|
PrivateTmp = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
};
|
||||||
|
|
||||||
path = with pkgs; [
|
environment = {
|
||||||
nix
|
FC_CONFIG_FILE = "${settingsFile}";
|
||||||
];
|
};
|
||||||
|
};
|
||||||
|
|
||||||
serviceConfig = {
|
fc-evaluator = mkIf cfg.evaluator.enable {
|
||||||
ExecStart = "${cfg.queueRunnerPackage}/bin/fc-queue-runner";
|
description = "FC CI Evaluator";
|
||||||
Restart = "on-failure";
|
wantedBy = ["multi-user.target"];
|
||||||
RestartSec = 10;
|
after = ["network.target" "fc-server.service"] ++ optional cfg.database.createLocally "postgresql.target";
|
||||||
User = "fc";
|
requires = ["fc-server.service"] ++ optional cfg.database.createLocally "postgresql.target";
|
||||||
Group = "fc";
|
|
||||||
StateDirectory = "fc";
|
|
||||||
LogsDirectory = "fc";
|
|
||||||
WorkingDirectory = "/var/lib/fc";
|
|
||||||
ReadWritePaths = [
|
|
||||||
"/var/lib/fc"
|
|
||||||
"/nix/var/nix/gcroots/per-user/fc"
|
|
||||||
];
|
|
||||||
|
|
||||||
# Hardening
|
path = with pkgs; [
|
||||||
ProtectSystem = "strict";
|
nix
|
||||||
ProtectHome = true;
|
git
|
||||||
NoNewPrivileges = true;
|
nix-eval-jobs
|
||||||
PrivateTmp = true;
|
];
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
environment = {
|
serviceConfig = {
|
||||||
FC_CONFIG_FILE = "${settingsFile}";
|
ExecStart = "${cfg.evaluatorPackage}/bin/fc-evaluator";
|
||||||
FC_QUEUE_RUNNER__WORK_DIR = "/var/lib/fc/queue-runner";
|
Restart = "on-failure";
|
||||||
|
RestartSec = 10;
|
||||||
|
User = "fc";
|
||||||
|
Group = "fc";
|
||||||
|
StateDirectory = "fc";
|
||||||
|
WorkingDirectory = "/var/lib/fc";
|
||||||
|
ReadWritePaths = ["/var/lib/fc"];
|
||||||
|
|
||||||
|
# Hardening
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
FC_CONFIG_FILE = "${settingsFile}";
|
||||||
|
FC_EVALUATOR__WORK_DIR = "/var/lib/fc/evaluator";
|
||||||
|
FC_EVALUATOR__RESTRICT_EVAL = "true";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fc-queue-runner = mkIf cfg.queueRunner.enable {
|
||||||
|
description = "FC CI Queue Runner";
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
after = ["network.target" "fc-server.service"] ++ optional cfg.database.createLocally "postgresql.target";
|
||||||
|
requires = ["fc-server.service"] ++ optional cfg.database.createLocally "postgresql.target";
|
||||||
|
|
||||||
|
path = with pkgs; [
|
||||||
|
nix
|
||||||
|
];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "${cfg.queueRunnerPackage}/bin/fc-queue-runner";
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = 10;
|
||||||
|
User = "fc";
|
||||||
|
Group = "fc";
|
||||||
|
StateDirectory = "fc";
|
||||||
|
LogsDirectory = "fc";
|
||||||
|
WorkingDirectory = "/var/lib/fc";
|
||||||
|
ReadWritePaths = [
|
||||||
|
"/var/lib/fc"
|
||||||
|
"/nix/var/nix/gcroots/per-user/fc"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Hardening
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
FC_CONFIG_FILE = "${settingsFile}";
|
||||||
|
FC_QUEUE_RUNNER__WORK_DIR = "/var/lib/fc/queue-runner";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
# API CRUD tests: dashboard content, project/jobset/evaluation/build/channel/builder CRUD, admin endpoints, pagination, search
|
{pkgs, self}:
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
fc-packages,
|
|
||||||
nixosModule,
|
|
||||||
}:
|
|
||||||
pkgs.testers.nixosTest {
|
pkgs.testers.nixosTest {
|
||||||
name = "fc-api-crud";
|
name = "fc-api-crud";
|
||||||
|
|
||||||
nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;};
|
nodes.machine = {
|
||||||
|
imports = [
|
||||||
|
self.nixosModules.fc-ci
|
||||||
|
../vm-common.nix
|
||||||
|
];
|
||||||
|
_module.args.self = self;
|
||||||
|
};
|
||||||
|
|
||||||
|
# API CRUD tests: dashboard content, project/jobset/evaluation/build/channel/builder
|
||||||
|
# CRUD, admin endpoints, pagination, search
|
||||||
testScript = ''
|
testScript = ''
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
|
@ -24,7 +27,7 @@ pkgs.testers.nixosTest {
|
||||||
# Wait for the server to start listening
|
# Wait for the server to start listening
|
||||||
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30)
|
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30)
|
||||||
|
|
||||||
# ---- Seed an API key for write operations ----
|
# Seed an API key for write operations
|
||||||
# Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table
|
# Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table
|
||||||
api_token = "fc_testkey123"
|
api_token = "fc_testkey123"
|
||||||
api_hash = hashlib.sha256(api_token.encode()).hexdigest()
|
api_hash = hashlib.sha256(api_token.encode()).hexdigest()
|
||||||
|
|
@ -51,11 +54,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
project_id = result.strip()
|
project_id = result.strip()
|
||||||
|
|
||||||
# ========================================================================
|
# Dashboard content verification
|
||||||
# Phase 4: Dashboard Content & Deep Functional Tests
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
# ---- 4A: Dashboard content verification ----
|
|
||||||
with subtest("Home page contains Dashboard heading"):
|
with subtest("Home page contains Dashboard heading"):
|
||||||
body = machine.succeed("curl -sf http://127.0.0.1:3000/")
|
body = machine.succeed("curl -sf http://127.0.0.1:3000/")
|
||||||
assert "Dashboard" in body, "Home page missing 'Dashboard' heading"
|
assert "Dashboard" in body, "Home page missing 'Dashboard' heading"
|
||||||
|
|
@ -111,7 +110,7 @@ pkgs.testers.nixosTest {
|
||||||
assert "api_key" in body or "API" in body, "Login page missing API key input"
|
assert "api_key" in body or "API" in body, "Login page missing API key input"
|
||||||
assert "<form" in body.lower(), "Login page missing form element"
|
assert "<form" in body.lower(), "Login page missing form element"
|
||||||
|
|
||||||
# ---- 4B: Dashboard page for specific entities ----
|
# Dashboard page for specific entities
|
||||||
with subtest("Project detail page renders for existing project"):
|
with subtest("Project detail page renders for existing project"):
|
||||||
body = machine.succeed(
|
body = machine.succeed(
|
||||||
f"curl -sf http://127.0.0.1:3000/project/{project_id}"
|
f"curl -sf http://127.0.0.1:3000/project/{project_id}"
|
||||||
|
|
@ -126,7 +125,7 @@ pkgs.testers.nixosTest {
|
||||||
# Should return 200 with "not found" message or similar, not crash
|
# Should return 200 with "not found" message or similar, not crash
|
||||||
assert code.strip() == "200", f"Expected 200 for missing project detail, got {code.strip()}"
|
assert code.strip() == "200", f"Expected 200 for missing project detail, got {code.strip()}"
|
||||||
|
|
||||||
# ---- 4C: Project update via PUT ----
|
# Project update via PUT
|
||||||
with subtest("Update project description via PUT"):
|
with subtest("Update project description via PUT"):
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
"curl -s -o /dev/null -w '%{http_code}' "
|
"curl -s -o /dev/null -w '%{http_code}' "
|
||||||
|
|
@ -153,7 +152,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert code.strip() == "403", f"Expected 403 for read-only PUT, got {code.strip()}"
|
assert code.strip() == "403", f"Expected 403 for read-only PUT, got {code.strip()}"
|
||||||
|
|
||||||
# ---- 4D: Jobset CRUD ----
|
# Jobset CRUD
|
||||||
with subtest("Create jobset for test-project"):
|
with subtest("Create jobset for test-project"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
f"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects/{project_id}/jobsets "
|
f"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects/{project_id}/jobsets "
|
||||||
|
|
@ -177,7 +176,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert "main" in body, "Jobset detail page should show jobset name"
|
assert "main" in body, "Jobset detail page should show jobset name"
|
||||||
|
|
||||||
# ---- 4E: Evaluation trigger and lifecycle ----
|
# Evaluation trigger and lifecycle
|
||||||
with subtest("Trigger evaluation via API"):
|
with subtest("Trigger evaluation via API"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -sf -X POST http://127.0.0.1:3000/api/v1/evaluations/trigger "
|
"curl -sf -X POST http://127.0.0.1:3000/api/v1/evaluations/trigger "
|
||||||
|
|
@ -217,7 +216,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert code.strip() == "403", f"Expected 403 for read-only eval trigger, got {code.strip()}"
|
assert code.strip() == "403", f"Expected 403 for read-only eval trigger, got {code.strip()}"
|
||||||
|
|
||||||
# ---- 4E2: Build lifecycle (restart, bump) ----
|
# Build lifecycle (restart, bump)
|
||||||
# Create a build via SQL since builds are normally created by the evaluator
|
# Create a build via SQL since builds are normally created by the evaluator
|
||||||
with subtest("Create test build via SQL"):
|
with subtest("Create test build via SQL"):
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
|
|
@ -284,7 +283,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert "cancelled" in result.strip().lower(), f"Expected cancelled, got: {result.strip()}"
|
assert "cancelled" in result.strip().lower(), f"Expected cancelled, got: {result.strip()}"
|
||||||
|
|
||||||
# ---- 4E3: Evaluation comparison ----
|
# Evaluation comparison ----
|
||||||
with subtest("Trigger second evaluation for comparison"):
|
with subtest("Trigger second evaluation for comparison"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -sf -X POST http://127.0.0.1:3000/api/v1/evaluations/trigger "
|
"curl -sf -X POST http://127.0.0.1:3000/api/v1/evaluations/trigger "
|
||||||
|
|
@ -318,7 +317,7 @@ pkgs.testers.nixosTest {
|
||||||
assert len(data["new_jobs"]) >= 1, f"Expected at least 1 new job, got {data['new_jobs']}"
|
assert len(data["new_jobs"]) >= 1, f"Expected at least 1 new job, got {data['new_jobs']}"
|
||||||
assert any(j["job_name"] == "new-pkg" for j in data["new_jobs"]), "new-pkg should be in new_jobs"
|
assert any(j["job_name"] == "new-pkg" for j in data["new_jobs"]), "new-pkg should be in new_jobs"
|
||||||
|
|
||||||
# ---- 4F: Channel CRUD lifecycle ----
|
# Channel CRUD lifecycle ----
|
||||||
with subtest("Create channel via API"):
|
with subtest("Create channel via API"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -sf -X POST http://127.0.0.1:3000/api/v1/channels "
|
"curl -sf -X POST http://127.0.0.1:3000/api/v1/channels "
|
||||||
|
|
@ -382,7 +381,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert code.strip() == "200", f"Expected 200 for channel delete, got {code.strip()}"
|
assert code.strip() == "200", f"Expected 200 for channel delete, got {code.strip()}"
|
||||||
|
|
||||||
# ---- 4G: Remote builder CRUD lifecycle ----
|
# Remote builder CRUD lifecycle
|
||||||
with subtest("List remote builders"):
|
with subtest("List remote builders"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -sf http://127.0.0.1:3000/api/v1/admin/builders | jq 'length'"
|
"curl -sf http://127.0.0.1:3000/api/v1/admin/builders | jq 'length'"
|
||||||
|
|
@ -456,7 +455,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert code.strip() == "200", f"Expected 200 for builder delete, got {code.strip()}"
|
assert code.strip() == "200", f"Expected 200 for builder delete, got {code.strip()}"
|
||||||
|
|
||||||
# ---- 4H: Admin system status endpoint ----
|
# Admin system status endpoint
|
||||||
with subtest("System status endpoint requires admin"):
|
with subtest("System status endpoint requires admin"):
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
"curl -s -o /dev/null -w '%{http_code}' "
|
"curl -s -o /dev/null -w '%{http_code}' "
|
||||||
|
|
@ -473,7 +472,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert int(result.strip()) >= 1, f"Expected at least 1 project in system status, got {result.strip()}"
|
assert int(result.strip()) >= 1, f"Expected at least 1 project in system status, got {result.strip()}"
|
||||||
|
|
||||||
# ---- 4I: API key listing ----
|
# API key listing
|
||||||
with subtest("List API keys requires admin"):
|
with subtest("List API keys requires admin"):
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
"curl -s -o /dev/null -w '%{http_code}' "
|
"curl -s -o /dev/null -w '%{http_code}' "
|
||||||
|
|
@ -490,7 +489,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert int(result.strip()) >= 1, f"Expected at least 1 API key, got {result.strip()}"
|
assert int(result.strip()) >= 1, f"Expected at least 1 API key, got {result.strip()}"
|
||||||
|
|
||||||
# ---- 4J: Badge endpoints ----
|
# Badge endpoints
|
||||||
with subtest("Badge endpoint returns SVG for unknown project"):
|
with subtest("Badge endpoint returns SVG for unknown project"):
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
"curl -s -o /dev/null -w '%{http_code}' "
|
"curl -s -o /dev/null -w '%{http_code}' "
|
||||||
|
|
@ -523,7 +522,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert code.strip() in ("404", "500"), f"Expected 404/500 for latest build, got {code.strip()}"
|
assert code.strip() in ("404", "500"), f"Expected 404/500 for latest build, got {code.strip()}"
|
||||||
|
|
||||||
# ---- 4K: Pagination tests ----
|
# Pagination tests
|
||||||
# Re-verify server is healthy before pagination tests
|
# Re-verify server is healthy before pagination tests
|
||||||
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=15)
|
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=15)
|
||||||
|
|
||||||
|
|
@ -555,7 +554,7 @@ pkgs.testers.nixosTest {
|
||||||
assert "limit" in data, f"Expected paginated response with 'limit' field, got: {result[:300]}"
|
assert "limit" in data, f"Expected paginated response with 'limit' field, got: {result[:300]}"
|
||||||
assert data["limit"] == 2, f"Expected limit=2, got {data['limit']}"
|
assert data["limit"] == 2, f"Expected limit=2, got {data['limit']}"
|
||||||
|
|
||||||
# ---- 4L: Build sub-resources ----
|
# Build sub-resources
|
||||||
with subtest("Build steps endpoint returns empty array for nonexistent build"):
|
with subtest("Build steps endpoint returns empty array for nonexistent build"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -sf "
|
"curl -sf "
|
||||||
|
|
@ -579,7 +578,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert code.strip() == "404", f"Expected 404 for nonexistent build log, got {code.strip()}"
|
assert code.strip() == "404", f"Expected 404 for nonexistent build log, got {code.strip()}"
|
||||||
|
|
||||||
# ---- 4M: Search functionality ----
|
# Search functionality
|
||||||
with subtest("Search returns matching projects"):
|
with subtest("Search returns matching projects"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -sf 'http://127.0.0.1:3000/api/v1/search?q=test-project' | jq '.projects | length'"
|
"curl -sf 'http://127.0.0.1:3000/api/v1/search?q=test-project' | jq '.projects | length'"
|
||||||
|
|
@ -592,7 +591,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert result.strip() == "0", f"Expected 0, got {result.strip()}"
|
assert result.strip() == "0", f"Expected 0, got {result.strip()}"
|
||||||
|
|
||||||
# ---- 4N: Content-Type verification for API endpoints ----
|
# Content-Type verification for API endpoints
|
||||||
with subtest("API endpoints return application/json"):
|
with subtest("API endpoints return application/json"):
|
||||||
ct = machine.succeed(
|
ct = machine.succeed(
|
||||||
"curl -s -D - -o /dev/null http://127.0.0.1:3000/api/v1/projects | grep -i content-type"
|
"curl -s -D - -o /dev/null http://127.0.0.1:3000/api/v1/projects | grep -i content-type"
|
||||||
|
|
@ -611,7 +610,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert "text/plain" in ct.lower() or "text/" in ct.lower(), f"Expected text content type for metrics, got: {ct}"
|
assert "text/plain" in ct.lower() or "text/" in ct.lower(), f"Expected text content type for metrics, got: {ct}"
|
||||||
|
|
||||||
# ---- 4O: Session/Cookie auth for dashboard ----
|
# Session/Cookie auth for dashboard
|
||||||
with subtest("Login with valid API key sets session cookie"):
|
with subtest("Login with valid API key sets session cookie"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -s -D - -o /dev/null "
|
"curl -s -D - -o /dev/null "
|
||||||
|
|
@ -661,7 +660,7 @@ pkgs.testers.nixosTest {
|
||||||
assert "Max-Age=0" in result or "max-age=0" in result.lower(), \
|
assert "Max-Age=0" in result or "max-age=0" in result.lower(), \
|
||||||
"Logout should set Max-Age=0 to clear cookie"
|
"Logout should set Max-Age=0 to clear cookie"
|
||||||
|
|
||||||
# ---- 4P: RBAC with create-projects role ----
|
# RBAC with create-projects role
|
||||||
cp_token = "fc_createprojects_key"
|
cp_token = "fc_createprojects_key"
|
||||||
cp_hash = hashlib.sha256(cp_token.encode()).hexdigest()
|
cp_hash = hashlib.sha256(cp_token.encode()).hexdigest()
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
|
|
@ -709,7 +708,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert code.strip() == "403", f"Expected 403 for create-projects system status, got {code.strip()}"
|
assert code.strip() == "403", f"Expected 403 for create-projects system status, got {code.strip()}"
|
||||||
|
|
||||||
# ---- 4Q: Additional security tests ----
|
# Additional security tests
|
||||||
with subtest("DELETE project without auth returns 401"):
|
with subtest("DELETE project without auth returns 401"):
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
"curl -s -o /dev/null -w '%{http_code}' "
|
"curl -s -o /dev/null -w '%{http_code}' "
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
# Authentication and RBAC tests
|
{pkgs, self}:
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
fc-packages,
|
|
||||||
nixosModule,
|
|
||||||
}:
|
|
||||||
pkgs.testers.nixosTest {
|
pkgs.testers.nixosTest {
|
||||||
name = "fc-auth-rbac";
|
name = "fc-auth-rbac";
|
||||||
|
|
||||||
nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;};
|
nodes.machine = {
|
||||||
|
imports = [
|
||||||
|
self.nixosModules.fc-ci
|
||||||
|
../vm-common.nix
|
||||||
|
];
|
||||||
|
_module.args.self = self;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Authentication and RBAC tests
|
||||||
testScript = ''
|
testScript = ''
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
{
|
{pkgs, self}:
|
||||||
pkgs,
|
|
||||||
fc-packages,
|
|
||||||
nixosModule,
|
|
||||||
}:
|
|
||||||
pkgs.testers.nixosTest {
|
pkgs.testers.nixosTest {
|
||||||
name = "fc-basic-api";
|
name = "fc-basic-api";
|
||||||
|
|
||||||
nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;};
|
nodes.machine = {
|
||||||
|
imports = [
|
||||||
|
self.nixosModules.fc-ci
|
||||||
|
../vm-common.nix
|
||||||
|
];
|
||||||
|
_module.args.self = self;
|
||||||
|
};
|
||||||
|
|
||||||
testScript = ''
|
testScript = ''
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
@ -22,7 +24,7 @@ pkgs.testers.nixosTest {
|
||||||
# Wait for the server to start listening
|
# Wait for the server to start listening
|
||||||
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30)
|
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30)
|
||||||
|
|
||||||
# ---- Seed an API key for write operations ----
|
## Seed an API key for write operations
|
||||||
# Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table
|
# Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table
|
||||||
api_token = "fc_testkey123"
|
api_token = "fc_testkey123"
|
||||||
api_hash = hashlib.sha256(api_token.encode()).hexdigest()
|
api_hash = hashlib.sha256(api_token.encode()).hexdigest()
|
||||||
|
|
@ -31,7 +33,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
auth_header = f"-H 'Authorization: Bearer {api_token}'"
|
auth_header = f"-H 'Authorization: Bearer {api_token}'"
|
||||||
|
|
||||||
# ---- Health endpoint ----
|
# Health endpoint
|
||||||
with subtest("Health endpoint returns OK"):
|
with subtest("Health endpoint returns OK"):
|
||||||
result = machine.succeed("curl -sf http://127.0.0.1:3000/health | jq -r .status")
|
result = machine.succeed("curl -sf http://127.0.0.1:3000/health | jq -r .status")
|
||||||
assert result.strip() == "ok", f"Expected 'ok', got '{result.strip()}'"
|
assert result.strip() == "ok", f"Expected 'ok', got '{result.strip()}'"
|
||||||
|
|
@ -40,13 +42,13 @@ pkgs.testers.nixosTest {
|
||||||
result = machine.succeed("curl -sf http://127.0.0.1:3000/health | jq -r .database")
|
result = machine.succeed("curl -sf http://127.0.0.1:3000/health | jq -r .database")
|
||||||
assert result.strip() == "true", f"Expected 'true', got '{result.strip()}'"
|
assert result.strip() == "true", f"Expected 'true', got '{result.strip()}'"
|
||||||
|
|
||||||
# ---- Cache endpoint: nix-cache-info ----
|
# Cache endpoint: nix-cache-info
|
||||||
with subtest("Cache info endpoint returns correct data"):
|
with subtest("Cache info endpoint returns correct data"):
|
||||||
result = machine.succeed("curl -sf http://127.0.0.1:3000/nix-cache/nix-cache-info")
|
result = machine.succeed("curl -sf http://127.0.0.1:3000/nix-cache/nix-cache-info")
|
||||||
assert "StoreDir: /nix/store" in result, f"Missing StoreDir in: {result}"
|
assert "StoreDir: /nix/store" in result, f"Missing StoreDir in: {result}"
|
||||||
assert "WantMassQuery: 1" in result, f"Missing WantMassQuery in: {result}"
|
assert "WantMassQuery: 1" in result, f"Missing WantMassQuery in: {result}"
|
||||||
|
|
||||||
# ---- Cache endpoint: invalid hash rejection ----
|
# Cache endpoint: invalid hash rejection
|
||||||
with subtest("Cache rejects short hash"):
|
with subtest("Cache rejects short hash"):
|
||||||
machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/tooshort.narinfo | grep -q 404")
|
machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/tooshort.narinfo | grep -q 404")
|
||||||
|
|
||||||
|
|
@ -59,14 +61,14 @@ pkgs.testers.nixosTest {
|
||||||
with subtest("Cache returns 404 for valid but nonexistent hash"):
|
with subtest("Cache returns 404 for valid but nonexistent hash"):
|
||||||
machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/abcdefghijklmnopqrstuvwxyz012345.narinfo | grep -q 404")
|
machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/abcdefghijklmnopqrstuvwxyz012345.narinfo | grep -q 404")
|
||||||
|
|
||||||
# ---- NAR endpoints: invalid hash rejection ----
|
# NAR endpoints: invalid hash rejection
|
||||||
with subtest("NAR zst rejects invalid hash"):
|
with subtest("NAR zst rejects invalid hash"):
|
||||||
machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/nar/INVALID.nar.zst | grep -q 404")
|
machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/nar/INVALID.nar.zst | grep -q 404")
|
||||||
|
|
||||||
with subtest("NAR plain rejects invalid hash"):
|
with subtest("NAR plain rejects invalid hash"):
|
||||||
machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/nar/INVALID.nar | grep -q 404")
|
machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/nar/INVALID.nar | grep -q 404")
|
||||||
|
|
||||||
# ---- Search endpoint: length validation ----
|
# Search endpoint: length validation
|
||||||
with subtest("Search rejects empty query"):
|
with subtest("Search rejects empty query"):
|
||||||
result = machine.succeed("curl -sf 'http://127.0.0.1:3000/api/v1/search?q=' | jq '.projects | length'")
|
result = machine.succeed("curl -sf 'http://127.0.0.1:3000/api/v1/search?q=' | jq '.projects | length'")
|
||||||
assert result.strip() == "0", f"Expected 0 projects, got {result.strip()}"
|
assert result.strip() == "0", f"Expected 0 projects, got {result.strip()}"
|
||||||
|
|
@ -76,12 +78,12 @@ pkgs.testers.nixosTest {
|
||||||
result = machine.succeed(f"curl -sf 'http://127.0.0.1:3000/api/v1/search?q={long_q}' | jq '.projects | length'")
|
result = machine.succeed(f"curl -sf 'http://127.0.0.1:3000/api/v1/search?q={long_q}' | jq '.projects | length'")
|
||||||
assert result.strip() == "0", f"Expected 0 projects for long query, got {result.strip()}"
|
assert result.strip() == "0", f"Expected 0 projects for long query, got {result.strip()}"
|
||||||
|
|
||||||
# ---- Error response format ----
|
# Error response format
|
||||||
with subtest("404 error response includes error_code field"):
|
with subtest("404 error response includes error_code field"):
|
||||||
json_result = machine.succeed("curl -s http://127.0.0.1:3000/api/v1/projects/00000000-0000-0000-0000-000000000000 | jq -r .error_code")
|
json_result = machine.succeed("curl -s http://127.0.0.1:3000/api/v1/projects/00000000-0000-0000-0000-000000000000 | jq -r .error_code")
|
||||||
assert json_result.strip() == "NOT_FOUND", f"Expected NOT_FOUND, got {json_result.strip()}"
|
assert json_result.strip() == "NOT_FOUND", f"Expected NOT_FOUND, got {json_result.strip()}"
|
||||||
|
|
||||||
# ---- Empty page states (before any data is created) ----
|
# Empty page states (before any data is created)
|
||||||
with subtest("Empty evaluations page has proper empty state"):
|
with subtest("Empty evaluations page has proper empty state"):
|
||||||
body = machine.succeed("curl -sf http://127.0.0.1:3000/evaluations")
|
body = machine.succeed("curl -sf http://127.0.0.1:3000/evaluations")
|
||||||
assert "Page 1 of 0" not in body, \
|
assert "Page 1 of 0" not in body, \
|
||||||
|
|
@ -107,7 +109,7 @@ pkgs.testers.nixosTest {
|
||||||
assert "table-wrap" in body, \
|
assert "table-wrap" in body, \
|
||||||
"Projects page should wrap tables in .table-wrap class"
|
"Projects page should wrap tables in .table-wrap class"
|
||||||
|
|
||||||
# ---- API CRUD: create and list projects ----
|
# API CRUD: create and list projects
|
||||||
with subtest("Create a project via API"):
|
with subtest("Create a project via API"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects "
|
"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects "
|
||||||
|
|
@ -123,7 +125,7 @@ pkgs.testers.nixosTest {
|
||||||
result = machine.succeed("curl -sf http://127.0.0.1:3000/api/v1/projects | jq '.items[0].name'")
|
result = machine.succeed("curl -sf http://127.0.0.1:3000/api/v1/projects | jq '.items[0].name'")
|
||||||
assert "test-project" in result, f"Expected test-project in: {result}"
|
assert "test-project" in result, f"Expected test-project in: {result}"
|
||||||
|
|
||||||
# ---- Builds list with filters ----
|
# Builds list with filters
|
||||||
with subtest("Builds list with system filter returns 200"):
|
with subtest("Builds list with system filter returns 200"):
|
||||||
machine.succeed("curl -sf 'http://127.0.0.1:3000/api/v1/builds?system=x86_64-linux' | jq '.items'")
|
machine.succeed("curl -sf 'http://127.0.0.1:3000/api/v1/builds?system=x86_64-linux' | jq '.items'")
|
||||||
|
|
||||||
|
|
@ -133,14 +135,14 @@ pkgs.testers.nixosTest {
|
||||||
with subtest("Builds list with combined filters returns 200"):
|
with subtest("Builds list with combined filters returns 200"):
|
||||||
machine.succeed("curl -sf 'http://127.0.0.1:3000/api/v1/builds?system=x86_64-linux&status=pending&job_name=test' | jq '.items'")
|
machine.succeed("curl -sf 'http://127.0.0.1:3000/api/v1/builds?system=x86_64-linux&status=pending&job_name=test' | jq '.items'")
|
||||||
|
|
||||||
# ---- Metrics endpoint ----
|
# Metrics endpoint
|
||||||
with subtest("Metrics endpoint returns prometheus format"):
|
with subtest("Metrics endpoint returns prometheus format"):
|
||||||
result = machine.succeed("curl -sf http://127.0.0.1:3000/metrics")
|
result = machine.succeed("curl -sf http://127.0.0.1:3000/metrics")
|
||||||
assert "fc_builds_total" in result, "Missing fc_builds_total in metrics"
|
assert "fc_builds_total" in result, "Missing fc_builds_total in metrics"
|
||||||
assert "fc_projects_total" in result, "Missing fc_projects_total in metrics"
|
assert "fc_projects_total" in result, "Missing fc_projects_total in metrics"
|
||||||
assert "fc_evaluations_total" in result, "Missing fc_evaluations_total in metrics"
|
assert "fc_evaluations_total" in result, "Missing fc_evaluations_total in metrics"
|
||||||
|
|
||||||
# ---- CORS: default restrictive (no Access-Control-Allow-Origin for cross-origin) ----
|
# CORS: default restrictive (no Access-Control-Allow-Origin for cross-origin)
|
||||||
with subtest("Default CORS does not allow arbitrary origins"):
|
with subtest("Default CORS does not allow arbitrary origins"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -s -D - "
|
"curl -s -D - "
|
||||||
|
|
@ -153,7 +155,7 @@ pkgs.testers.nixosTest {
|
||||||
assert "access-control-allow-origin: http://evil.example.com" not in result.lower(), \
|
assert "access-control-allow-origin: http://evil.example.com" not in result.lower(), \
|
||||||
f"CORS should not allow arbitrary origins: {result}"
|
f"CORS should not allow arbitrary origins: {result}"
|
||||||
|
|
||||||
# ---- Systemd hardening ----
|
# Systemd hardening
|
||||||
with subtest("fc-server runs as fc user"):
|
with subtest("fc-server runs as fc user"):
|
||||||
result = machine.succeed("systemctl show fc-server --property=User --value")
|
result = machine.succeed("systemctl show fc-server --property=User --value")
|
||||||
assert result.strip() == "fc", f"Expected fc user, got '{result.strip()}'"
|
assert result.strip() == "fc", f"Expected fc user, got '{result.strip()}'"
|
||||||
|
|
@ -168,7 +170,7 @@ pkgs.testers.nixosTest {
|
||||||
with subtest("Log directory exists"):
|
with subtest("Log directory exists"):
|
||||||
machine.succeed("test -d /var/lib/fc/logs || mkdir -p /var/lib/fc/logs")
|
machine.succeed("test -d /var/lib/fc/logs || mkdir -p /var/lib/fc/logs")
|
||||||
|
|
||||||
# ---- Stats endpoint ----
|
# Stats endpoint
|
||||||
with subtest("Build stats endpoint returns data"):
|
with subtest("Build stats endpoint returns data"):
|
||||||
result = machine.succeed("curl -sf http://127.0.0.1:3000/api/v1/builds/stats | jq '.total_builds'")
|
result = machine.succeed("curl -sf http://127.0.0.1:3000/api/v1/builds/stats | jq '.total_builds'")
|
||||||
# Should be a number (possibly 0)
|
# Should be a number (possibly 0)
|
||||||
|
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
# Common machine configuration for all FC integration tests
|
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
fc-packages,
|
|
||||||
nixosModule,
|
|
||||||
}: {
|
|
||||||
imports = [nixosModule];
|
|
||||||
|
|
||||||
programs.git.enable = true;
|
|
||||||
security.sudo.enable = true;
|
|
||||||
|
|
||||||
# Ensure nix and zstd are available for cache endpoints
|
|
||||||
environment.systemPackages = with pkgs; [nix nix-eval-jobs zstd curl jq openssl];
|
|
||||||
|
|
||||||
services.fc = {
|
|
||||||
enable = true;
|
|
||||||
package = fc-packages.fc-server;
|
|
||||||
evaluatorPackage = fc-packages.fc-evaluator;
|
|
||||||
queueRunnerPackage = fc-packages.fc-queue-runner;
|
|
||||||
migratePackage = fc-packages.fc-migrate-cli;
|
|
||||||
|
|
||||||
server.enable = true;
|
|
||||||
evaluator.enable = true;
|
|
||||||
queueRunner.enable = true;
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
database.url = "postgresql:///fc?host=/run/postgresql";
|
|
||||||
server = {
|
|
||||||
host = "127.0.0.1";
|
|
||||||
port = 3000;
|
|
||||||
cors_permissive = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
gc.enabled = false;
|
|
||||||
logs.log_dir = "/var/lib/fc/logs";
|
|
||||||
cache.enabled = true;
|
|
||||||
signing.enabled = false;
|
|
||||||
|
|
||||||
tracing = {
|
|
||||||
level = "info";
|
|
||||||
format = "compact";
|
|
||||||
show_targets = true;
|
|
||||||
show_timestamps = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
evaluator.poll_interval = 5;
|
|
||||||
evaluator.work_dir = "/var/lib/fc/evaluator";
|
|
||||||
queue_runner.poll_interval = 3;
|
|
||||||
queue_runner.work_dir = "/var/lib/fc/queue-runner";
|
|
||||||
|
|
||||||
# Declarative bootstrap: project + API key created on startup
|
|
||||||
declarative = {
|
|
||||||
projects = [
|
|
||||||
{
|
|
||||||
name = "declarative-project";
|
|
||||||
repository_url = "https://github.com/test/declarative";
|
|
||||||
description = "Bootstrap test project";
|
|
||||||
jobsets = [
|
|
||||||
{
|
|
||||||
name = "packages";
|
|
||||||
nix_expression = "packages";
|
|
||||||
enabled = true;
|
|
||||||
flake_mode = true;
|
|
||||||
check_interval = 300;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
api_keys = [
|
|
||||||
{
|
|
||||||
name = "bootstrap-admin";
|
|
||||||
key = "fc_bootstrap_key";
|
|
||||||
role = "admin";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
# End-to-end tests: flake creation, evaluation, queue runner, notification, signing, GC, declarative, webhooks
|
{pkgs, self}:
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
fc-packages,
|
|
||||||
nixosModule,
|
|
||||||
}:
|
|
||||||
pkgs.testers.nixosTest {
|
pkgs.testers.nixosTest {
|
||||||
name = "fc-e2e";
|
name = "fc-e2e";
|
||||||
|
|
||||||
nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;};
|
nodes.machine = {
|
||||||
|
imports = [
|
||||||
|
self.nixosModules.fc-ci
|
||||||
|
../vm-common.nix
|
||||||
|
];
|
||||||
|
_module.args.self = self;
|
||||||
|
};
|
||||||
|
|
||||||
|
# End-to-end tests: flake creation, evaluation, queue runner, notification,
|
||||||
|
# signing, GC, declarative, webhooks
|
||||||
testScript = ''
|
testScript = ''
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
|
@ -24,7 +27,7 @@ pkgs.testers.nixosTest {
|
||||||
# Wait for the server to start listening
|
# Wait for the server to start listening
|
||||||
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30)
|
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30)
|
||||||
|
|
||||||
# ---- Seed an API key for write operations ----
|
# Seed an API key for write operations
|
||||||
# Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table
|
# Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table
|
||||||
api_token = "fc_testkey123"
|
api_token = "fc_testkey123"
|
||||||
api_hash = hashlib.sha256(api_token.encode()).hexdigest()
|
api_hash = hashlib.sha256(api_token.encode()).hexdigest()
|
||||||
|
|
@ -41,11 +44,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
ro_header = f"-H 'Authorization: Bearer {ro_token}'"
|
ro_header = f"-H 'Authorization: Bearer {ro_token}'"
|
||||||
|
|
||||||
# ========================================================================
|
# Create a test flake inside the VM
|
||||||
# Phase E2E-1: End-to-End Evaluator Integration Test
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
# ---- Create a test flake inside the VM ----
|
|
||||||
with subtest("Create bare git repo with test flake"):
|
with subtest("Create bare git repo with test flake"):
|
||||||
machine.succeed("mkdir -p /var/lib/fc/test-repos")
|
machine.succeed("mkdir -p /var/lib/fc/test-repos")
|
||||||
machine.succeed("git init --bare /var/lib/fc/test-repos/test-flake.git")
|
machine.succeed("git init --bare /var/lib/fc/test-repos/test-flake.git")
|
||||||
|
|
@ -78,7 +77,7 @@ pkgs.testers.nixosTest {
|
||||||
# Set ownership for fc user
|
# Set ownership for fc user
|
||||||
machine.succeed("chown -R fc:fc /var/lib/fc/test-repos")
|
machine.succeed("chown -R fc:fc /var/lib/fc/test-repos")
|
||||||
|
|
||||||
# ---- Create project + jobset pointing to the local repo via API ----
|
# Create project + jobset pointing to the local repo via API
|
||||||
with subtest("Create E2E project and jobset via API"):
|
with subtest("Create E2E project and jobset via API"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects "
|
"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects "
|
||||||
|
|
@ -100,10 +99,10 @@ pkgs.testers.nixosTest {
|
||||||
e2e_jobset_id = result.strip()
|
e2e_jobset_id = result.strip()
|
||||||
assert len(e2e_jobset_id) == 36, f"Expected UUID for jobset, got '{e2e_jobset_id}'"
|
assert len(e2e_jobset_id) == 36, f"Expected UUID for jobset, got '{e2e_jobset_id}'"
|
||||||
|
|
||||||
# ---- Wait for evaluator to pick it up and create an evaluation ----
|
# Wait for evaluator to pick it up and create an evaluation
|
||||||
with subtest("Evaluator discovers and evaluates the flake"):
|
with subtest("Evaluator discovers and evaluates the flake"):
|
||||||
# The evaluator is already running (started in Phase 1)
|
# The evaluator is already running, poll for evaluation to appear
|
||||||
# Poll for evaluation to appear with status "completed"
|
# with status "completed"
|
||||||
machine.wait_until_succeeds(
|
machine.wait_until_succeeds(
|
||||||
f"curl -sf 'http://127.0.0.1:3000/api/v1/evaluations?jobset_id={e2e_jobset_id}' "
|
f"curl -sf 'http://127.0.0.1:3000/api/v1/evaluations?jobset_id={e2e_jobset_id}' "
|
||||||
"| jq -e '.items[] | select(.status==\"completed\")'",
|
"| jq -e '.items[] | select(.status==\"completed\")'",
|
||||||
|
|
@ -136,7 +135,7 @@ pkgs.testers.nixosTest {
|
||||||
f"curl -sf 'http://127.0.0.1:3000/api/v1/builds?evaluation_id={e2e_eval_id}' | jq -r '.items[0].id'"
|
f"curl -sf 'http://127.0.0.1:3000/api/v1/builds?evaluation_id={e2e_eval_id}' | jq -r '.items[0].id'"
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
# ---- Test evaluation caching ----
|
# Test evaluation caching
|
||||||
with subtest("Same commit does not trigger a new evaluation"):
|
with subtest("Same commit does not trigger a new evaluation"):
|
||||||
# Get current evaluation count
|
# Get current evaluation count
|
||||||
before_count = machine.succeed(
|
before_count = machine.succeed(
|
||||||
|
|
@ -149,7 +148,7 @@ pkgs.testers.nixosTest {
|
||||||
).strip()
|
).strip()
|
||||||
assert before_count == after_count, f"Evaluation count changed from {before_count} to {after_count} (should be cached)"
|
assert before_count == after_count, f"Evaluation count changed from {before_count} to {after_count} (should be cached)"
|
||||||
|
|
||||||
# ---- Test new commit triggers new evaluation ----
|
# Test new commit triggers new evaluation
|
||||||
with subtest("New commit triggers new evaluation"):
|
with subtest("New commit triggers new evaluation"):
|
||||||
before_count_int = int(machine.succeed(
|
before_count_int = int(machine.succeed(
|
||||||
f"curl -sf 'http://127.0.0.1:3000/api/v1/evaluations?jobset_id={e2e_jobset_id}' | jq '.items | length'"
|
f"curl -sf 'http://127.0.0.1:3000/api/v1/evaluations?jobset_id={e2e_jobset_id}' | jq '.items | length'"
|
||||||
|
|
@ -181,10 +180,6 @@ pkgs.testers.nixosTest {
|
||||||
timeout=60
|
timeout=60
|
||||||
)
|
)
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Phase E2E-2: End-to-End Queue Runner Integration Test
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("Queue runner builds pending derivation"):
|
with subtest("Queue runner builds pending derivation"):
|
||||||
# Poll the E2E build until completed (queue-runner is already running)
|
# Poll the E2E build until completed (queue-runner is already running)
|
||||||
machine.wait_until_succeeds(
|
machine.wait_until_succeeds(
|
||||||
|
|
@ -230,10 +225,6 @@ pkgs.testers.nixosTest {
|
||||||
).strip()
|
).strip()
|
||||||
assert code == "200", f"Expected 200 for build log, got {code}"
|
assert code == "200", f"Expected 200 for build log, got {code}"
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Phase E2E-3: Jobset Input Management API
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("Create jobset input via API"):
|
with subtest("Create jobset input via API"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
f"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects/{e2e_project_id}/jobsets/{e2e_jobset_id}/inputs "
|
f"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects/{e2e_project_id}/jobsets/{e2e_jobset_id}/inputs "
|
||||||
|
|
@ -285,10 +276,6 @@ pkgs.testers.nixosTest {
|
||||||
).strip()
|
).strip()
|
||||||
assert code == "403", f"Expected 403 for read-only input delete, got {code}"
|
assert code == "403", f"Expected 403 for read-only input delete, got {code}"
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Phase E2E-4: Notification Dispatch
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
# Notifications are dispatched after builds complete (already tested above).
|
# Notifications are dispatched after builds complete (already tested above).
|
||||||
# Verify run_command notifications work:
|
# Verify run_command notifications work:
|
||||||
with subtest("Notification run_command is invoked on build completion"):
|
with subtest("Notification run_command is invoked on build completion"):
|
||||||
|
|
@ -300,10 +287,6 @@ pkgs.testers.nixosTest {
|
||||||
).strip()
|
).strip()
|
||||||
assert result == "completed", f"Expected completed after notification, got {result}"
|
assert result == "completed", f"Expected completed after notification, got {result}"
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Phase E2E-5: Channel Auto-Promotion
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("Channel auto-promotion after all builds complete"):
|
with subtest("Channel auto-promotion after all builds complete"):
|
||||||
# Create a channel tracking the E2E jobset
|
# Create a channel tracking the E2E jobset
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
|
|
@ -324,10 +307,6 @@ pkgs.testers.nixosTest {
|
||||||
timeout=30
|
timeout=30
|
||||||
)
|
)
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Phase E2E-6: Binary Cache NARinfo Test
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("Binary cache serves NARinfo for built output"):
|
with subtest("Binary cache serves NARinfo for built output"):
|
||||||
# Get the build output path
|
# Get the build output path
|
||||||
output_path = machine.succeed(
|
output_path = machine.succeed(
|
||||||
|
|
@ -353,10 +332,6 @@ pkgs.testers.nixosTest {
|
||||||
assert "StorePath:" in narinfo, f"NARinfo missing StorePath: {narinfo}"
|
assert "StorePath:" in narinfo, f"NARinfo missing StorePath: {narinfo}"
|
||||||
assert "NarHash:" in narinfo, f"NARinfo missing NarHash: {narinfo}"
|
assert "NarHash:" in narinfo, f"NARinfo missing NarHash: {narinfo}"
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Phase E2E-7: Build Retry on Failure
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("Build with invalid drv_path fails and retries"):
|
with subtest("Build with invalid drv_path fails and retries"):
|
||||||
# Insert a build with an invalid drv_path via SQL
|
# Insert a build with an invalid drv_path via SQL
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
|
|
@ -378,10 +353,6 @@ pkgs.testers.nixosTest {
|
||||||
).strip()
|
).strip()
|
||||||
assert result == "failed", f"Expected failed for bad build, got '{result}'"
|
assert result == "failed", f"Expected failed for bad build, got '{result}'"
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Phase E2E-8: Notification Dispatch (run_command)
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("Notification run_command invoked on build completion"):
|
with subtest("Notification run_command invoked on build completion"):
|
||||||
# Write a notification script
|
# Write a notification script
|
||||||
machine.succeed("mkdir -p /var/lib/fc")
|
machine.succeed("mkdir -p /var/lib/fc")
|
||||||
|
|
@ -458,10 +429,6 @@ pkgs.testers.nixosTest {
|
||||||
f"Expected BUILD_STATUS in notification output, got: {output}"
|
f"Expected BUILD_STATUS in notification output, got: {output}"
|
||||||
assert notify_build_id in output, f"Expected build ID {notify_build_id} in output, got: {output}"
|
assert notify_build_id in output, f"Expected build ID {notify_build_id} in output, got: {output}"
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Phase E2E-9: Nix Signing
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("Generate signing key and configure signing"):
|
with subtest("Generate signing key and configure signing"):
|
||||||
# Generate a Nix signing key
|
# Generate a Nix signing key
|
||||||
machine.succeed("mkdir -p /var/lib/fc/keys")
|
machine.succeed("mkdir -p /var/lib/fc/keys")
|
||||||
|
|
@ -537,10 +504,6 @@ pkgs.testers.nixosTest {
|
||||||
# The verify command should succeed (exit 0) if signatures are valid
|
# The verify command should succeed (exit 0) if signatures are valid
|
||||||
machine.succeed(f"nix store verify --sigs-needed 1 {output_path}")
|
machine.succeed(f"nix store verify --sigs-needed 1 {output_path}")
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Phase E2E-10: GC Roots
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("GC roots are created for build products"):
|
with subtest("GC roots are created for build products"):
|
||||||
# Enable GC in config
|
# Enable GC in config
|
||||||
machine.succeed("""
|
machine.succeed("""
|
||||||
|
|
@ -607,18 +570,14 @@ pkgs.testers.nixosTest {
|
||||||
found_root = True
|
found_root = True
|
||||||
break
|
break
|
||||||
|
|
||||||
# We might have GC roots - this is expected behavior
|
# We might have GC roots, this is expected behavior
|
||||||
# The key is that the build output exists and is protected from GC
|
# The key thing is that the build output exists and is protected from GC
|
||||||
machine.succeed(f"test -e {gc_build_output}")
|
machine.succeed(f"test -e {gc_build_output}")
|
||||||
else:
|
else:
|
||||||
# If no GC roots yet, at least verify the build output exists
|
# If no GC roots yet, at least verify the build output exists
|
||||||
# GC roots might be created asynchronously
|
# GC roots might be created asynchronously
|
||||||
machine.succeed(f"test -e {gc_build_output}")
|
machine.succeed(f"test -e {gc_build_output}")
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Phase E2E-11: Declarative In-Repo Config
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("Declarative .fc.toml in repo auto-creates jobset"):
|
with subtest("Declarative .fc.toml in repo auto-creates jobset"):
|
||||||
# Add .fc.toml to the test repo with a new jobset definition
|
# Add .fc.toml to the test repo with a new jobset definition
|
||||||
machine.succeed("""
|
machine.succeed("""
|
||||||
|
|
@ -641,10 +600,6 @@ pkgs.testers.nixosTest {
|
||||||
timeout=60
|
timeout=60
|
||||||
)
|
)
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Phase E2E-12: Webhook Endpoint
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("Webhook endpoint accepts valid GitHub push"):
|
with subtest("Webhook endpoint accepts valid GitHub push"):
|
||||||
# Create a webhook config via SQL (no REST endpoint for creation)
|
# Create a webhook config via SQL (no REST endpoint for creation)
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
|
|
@ -697,7 +652,7 @@ pkgs.testers.nixosTest {
|
||||||
).strip()
|
).strip()
|
||||||
assert code == "401", f"Expected 401 for invalid webhook signature, got {code}"
|
assert code == "401", f"Expected 401 for invalid webhook signature, got {code}"
|
||||||
|
|
||||||
# ---- Cleanup: Delete project ----
|
# Cleanup: Delete project
|
||||||
with subtest("Delete E2E project"):
|
with subtest("Delete E2E project"):
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
"curl -s -o /dev/null -w '%{http_code}' "
|
"curl -s -o /dev/null -w '%{http_code}' "
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
# Feature tests: logging, CSS, setup wizard, probe, metrics improvements
|
{pkgs, self}:
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
fc-packages,
|
|
||||||
nixosModule,
|
|
||||||
}:
|
|
||||||
pkgs.testers.nixosTest {
|
pkgs.testers.nixosTest {
|
||||||
name = "fc-features";
|
name = "fc-features";
|
||||||
|
|
||||||
nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;};
|
nodes.machine = {
|
||||||
|
imports = [
|
||||||
|
self.nixosModules.fc-ci
|
||||||
|
../vm-common.nix
|
||||||
|
];
|
||||||
|
_module.args.self = self;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Feature tests: logging, CSS, setup wizard, probe, metrics improvements
|
||||||
testScript = ''
|
testScript = ''
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
|
@ -23,7 +25,7 @@ pkgs.testers.nixosTest {
|
||||||
# Wait for the server to start listening
|
# Wait for the server to start listening
|
||||||
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30)
|
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30)
|
||||||
|
|
||||||
# ---- Seed an API key for write operations ----
|
# Seed an API key for write operations
|
||||||
# Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table
|
# Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table
|
||||||
api_token = "fc_testkey123"
|
api_token = "fc_testkey123"
|
||||||
api_hash = hashlib.sha256(api_token.encode()).hexdigest()
|
api_hash = hashlib.sha256(api_token.encode()).hexdigest()
|
||||||
|
|
@ -40,11 +42,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
ro_header = f"-H 'Authorization: Bearer {ro_token}'"
|
ro_header = f"-H 'Authorization: Bearer {ro_token}'"
|
||||||
|
|
||||||
# ========================================================================
|
# Structured logging ----
|
||||||
# Phase 5: New Feature Tests (Structured Logging, Flake Probe, Setup Wizard, Dashboard)
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
# ---- 5A: Structured logging ----
|
|
||||||
with subtest("Server produces structured log output"):
|
with subtest("Server produces structured log output"):
|
||||||
# The server should log via tracing with the configured format
|
# The server should log via tracing with the configured format
|
||||||
result = machine.succeed("journalctl -u fc-server --no-pager -n 50 2>&1")
|
result = machine.succeed("journalctl -u fc-server --no-pager -n 50 2>&1")
|
||||||
|
|
@ -52,7 +50,7 @@ pkgs.testers.nixosTest {
|
||||||
assert "INFO" in result or "info" in result, \
|
assert "INFO" in result or "info" in result, \
|
||||||
"Expected structured log lines with INFO level in journalctl output"
|
"Expected structured log lines with INFO level in journalctl output"
|
||||||
|
|
||||||
# ---- 5B: Static CSS serving ----
|
# Static CSS serving ----
|
||||||
with subtest("Static CSS endpoint returns 200 with correct content type"):
|
with subtest("Static CSS endpoint returns 200 with correct content type"):
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
"curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/static/style.css"
|
"curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/static/style.css"
|
||||||
|
|
@ -63,7 +61,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert "text/css" in ct.lower(), f"Expected text/css, got: {ct}"
|
assert "text/css" in ct.lower(), f"Expected text/css, got: {ct}"
|
||||||
|
|
||||||
# ---- 5C: Setup wizard page ----
|
# Setup wizard page
|
||||||
with subtest("Setup wizard page returns 200"):
|
with subtest("Setup wizard page returns 200"):
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
"curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/projects/new"
|
"curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/projects/new"
|
||||||
|
|
@ -92,7 +90,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert '/projects/new' in body, "Projects page should link to /projects/new wizard"
|
assert '/projects/new' in body, "Projects page should link to /projects/new wizard"
|
||||||
|
|
||||||
# ---- 5D: Flake probe endpoint ----
|
# Flake probe endpoint
|
||||||
with subtest("Probe endpoint exists and requires POST"):
|
with subtest("Probe endpoint exists and requires POST"):
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
"curl -s -o /dev/null -w '%{http_code}' "
|
"curl -s -o /dev/null -w '%{http_code}' "
|
||||||
|
|
@ -116,7 +114,7 @@ pkgs.testers.nixosTest {
|
||||||
assert code.strip() in ("200", "408", "422", "500"), \
|
assert code.strip() in ("200", "408", "422", "500"), \
|
||||||
f"Expected 200/408/422/500 for probe of unreachable repo, got {code.strip()}"
|
f"Expected 200/408/422/500 for probe of unreachable repo, got {code.strip()}"
|
||||||
|
|
||||||
# ---- 5E: Setup endpoint ----
|
# Setup endpoint
|
||||||
with subtest("Setup endpoint exists and requires POST"):
|
with subtest("Setup endpoint exists and requires POST"):
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
"curl -s -o /dev/null -w '%{http_code}' "
|
"curl -s -o /dev/null -w '%{http_code}' "
|
||||||
|
|
@ -157,7 +155,7 @@ pkgs.testers.nixosTest {
|
||||||
f"{auth_header}"
|
f"{auth_header}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---- 5F: Dashboard improvements ----
|
# Dashboard improvements
|
||||||
with subtest("Home page has dashboard-grid two-column layout"):
|
with subtest("Home page has dashboard-grid two-column layout"):
|
||||||
body = machine.succeed("curl -sf http://127.0.0.1:3000/")
|
body = machine.succeed("curl -sf http://127.0.0.1:3000/")
|
||||||
assert "dashboard-grid" in body, "Home page should have dashboard-grid class"
|
assert "dashboard-grid" in body, "Home page should have dashboard-grid class"
|
||||||
|
|
@ -179,7 +177,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert "escapeHtml" in body, "Admin page JS should use escapeHtml"
|
assert "escapeHtml" in body, "Admin page JS should use escapeHtml"
|
||||||
|
|
||||||
# ---- 4R: Metrics reflect actual data ----
|
# Metrics reflect actual data
|
||||||
with subtest("Metrics fc_projects_total reflects created projects"):
|
with subtest("Metrics fc_projects_total reflects created projects"):
|
||||||
result = machine.succeed("curl -sf http://127.0.0.1:3000/metrics")
|
result = machine.succeed("curl -sf http://127.0.0.1:3000/metrics")
|
||||||
for line in result.split("\n"):
|
for line in result.split("\n"):
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
{
|
{pkgs, self}:
|
||||||
pkgs,
|
|
||||||
fc-packages,
|
|
||||||
nixosModule,
|
|
||||||
}:
|
|
||||||
pkgs.testers.nixosTest {
|
pkgs.testers.nixosTest {
|
||||||
name = "fc-service-startup";
|
name = "fc-service-startup";
|
||||||
|
|
||||||
nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;};
|
nodes.machine = {
|
||||||
|
imports = [
|
||||||
|
self.nixosModules.fc-ci
|
||||||
|
../vm-common.nix
|
||||||
|
];
|
||||||
|
_module.args.self = self;
|
||||||
|
};
|
||||||
|
|
||||||
testScript = ''
|
testScript = ''
|
||||||
machine.start()
|
machine.start()
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
# Webhook and PR integration tests
|
{pkgs, self}:
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
fc-packages,
|
|
||||||
nixosModule,
|
|
||||||
}:
|
|
||||||
pkgs.testers.nixosTest {
|
pkgs.testers.nixosTest {
|
||||||
name = "fc-webhooks";
|
name = "fc-webhooks";
|
||||||
|
|
||||||
nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;};
|
nodes.machine = {
|
||||||
|
imports = [
|
||||||
|
self.nixosModules.fc-ci
|
||||||
|
../vm-common.nix
|
||||||
|
];
|
||||||
|
_module.args.self = self;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Webhook and PR integration tests
|
||||||
testScript = ''
|
testScript = ''
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
|
@ -24,7 +26,7 @@ pkgs.testers.nixosTest {
|
||||||
# Wait for the server to start listening
|
# Wait for the server to start listening
|
||||||
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30)
|
machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30)
|
||||||
|
|
||||||
# ---- Seed an API key for write operations ----
|
# Seed an API key for write operations
|
||||||
api_token = "fc_testkey123"
|
api_token = "fc_testkey123"
|
||||||
api_hash = hashlib.sha256(api_token.encode()).hexdigest()
|
api_hash = hashlib.sha256(api_token.encode()).hexdigest()
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
|
|
@ -32,7 +34,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
auth_header = f"-H 'Authorization: Bearer {api_token}'"
|
auth_header = f"-H 'Authorization: Bearer {api_token}'"
|
||||||
|
|
||||||
# ---- Create a test project for webhook tests ----
|
# Create a test project for webhook tests
|
||||||
with subtest("Create test project for webhooks"):
|
with subtest("Create test project for webhooks"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects "
|
"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects "
|
||||||
|
|
@ -44,7 +46,7 @@ pkgs.testers.nixosTest {
|
||||||
project_id = result.strip()
|
project_id = result.strip()
|
||||||
assert len(project_id) == 36, f"Expected UUID, got '{project_id}'"
|
assert len(project_id) == 36, f"Expected UUID, got '{project_id}'"
|
||||||
|
|
||||||
# ---- Create a jobset for the project ----
|
# Create a jobset for the project
|
||||||
with subtest("Create jobset for webhook project"):
|
with subtest("Create jobset for webhook project"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
f"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects/{project_id}/jobsets "
|
f"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects/{project_id}/jobsets "
|
||||||
|
|
@ -56,10 +58,7 @@ pkgs.testers.nixosTest {
|
||||||
jobset_id = result.strip()
|
jobset_id = result.strip()
|
||||||
assert len(jobset_id) == 36, f"Expected UUID, got '{jobset_id}'"
|
assert len(jobset_id) == 36, f"Expected UUID, got '{jobset_id}'"
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# GitHub Webhook Tests
|
# GitHub Webhook Tests
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("GitHub webhook returns 404 when not configured"):
|
with subtest("GitHub webhook returns 404 when not configured"):
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
"curl -s -o /dev/null -w '%{http_code}' "
|
"curl -s -o /dev/null -w '%{http_code}' "
|
||||||
|
|
@ -196,10 +195,7 @@ pkgs.testers.nixosTest {
|
||||||
)
|
)
|
||||||
assert "draft" in result.lower(), f"Expected draft PR to be skipped, got: {result}"
|
assert "draft" in result.lower(), f"Expected draft PR to be skipped, got: {result}"
|
||||||
|
|
||||||
# ========================================================================
|
## GitLab Webhook Tests
|
||||||
# GitLab Webhook Tests
|
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
# Create a GitLab project
|
# Create a GitLab project
|
||||||
with subtest("Create GitLab test project"):
|
with subtest("Create GitLab test project"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
|
|
@ -314,10 +310,7 @@ pkgs.testers.nixosTest {
|
||||||
assert "draft" in result.lower() or "wip" in result.lower(), \
|
assert "draft" in result.lower() or "wip" in result.lower(), \
|
||||||
f"Expected draft MR to be skipped, got: {result}"
|
f"Expected draft MR to be skipped, got: {result}"
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Gitea/Forgejo Webhook Tests
|
# Gitea/Forgejo Webhook Tests
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("Create Gitea test project"):
|
with subtest("Create Gitea test project"):
|
||||||
result = machine.succeed(
|
result = machine.succeed(
|
||||||
"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects "
|
"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects "
|
||||||
|
|
@ -365,10 +358,7 @@ pkgs.testers.nixosTest {
|
||||||
assert int(count_after) > int(count_before), \
|
assert int(count_after) > int(count_before), \
|
||||||
"Expected new evaluation from Gitea push"
|
"Expected new evaluation from Gitea push"
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# OAuth Routes Existence Tests
|
# OAuth Routes Existence Tests
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("GitHub OAuth login route exists"):
|
with subtest("GitHub OAuth login route exists"):
|
||||||
# Should redirect or return 404 if not configured
|
# Should redirect or return 404 if not configured
|
||||||
code = machine.succeed(
|
code = machine.succeed(
|
||||||
|
|
@ -384,10 +374,7 @@ pkgs.testers.nixosTest {
|
||||||
# Should fail gracefully (no OAuth configured)
|
# Should fail gracefully (no OAuth configured)
|
||||||
assert code.strip() in ("400", "404", "500"), f"Expected error code, got {code.strip()}"
|
assert code.strip() in ("400", "404", "500"), f"Expected error code, got {code.strip()}"
|
||||||
|
|
||||||
# ========================================================================
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
# ========================================================================
|
|
||||||
|
|
||||||
with subtest("Cleanup test projects"):
|
with subtest("Cleanup test projects"):
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
f"curl -sf -X DELETE http://127.0.0.1:3000/api/v1/projects/{project_id} {auth_header}"
|
f"curl -sf -X DELETE http://127.0.0.1:3000/api/v1/projects/{project_id} {auth_header}"
|
||||||
|
|
|
||||||
83
nix/vm-common.nix
Normal file
83
nix/vm-common.nix
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
# Common VM configuration for FC integration tests
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
fc-packages = self.packages.${pkgs.stdenv.hostPlatform.system};
|
||||||
|
in {
|
||||||
|
# Common machine configuration for all FC integration tests
|
||||||
|
config = {
|
||||||
|
programs.git.enable = true;
|
||||||
|
security.sudo.enable = true;
|
||||||
|
|
||||||
|
# Ensure nix and zstd are available for cache endpoints
|
||||||
|
environment.systemPackages = with pkgs; [nix nix-eval-jobs zstd curl jq openssl];
|
||||||
|
|
||||||
|
services.fc = {
|
||||||
|
enable = true;
|
||||||
|
package = fc-packages.fc-server;
|
||||||
|
evaluatorPackage = fc-packages.fc-evaluator;
|
||||||
|
queueRunnerPackage = fc-packages.fc-queue-runner;
|
||||||
|
migratePackage = fc-packages.fc-migrate-cli;
|
||||||
|
|
||||||
|
server.enable = true;
|
||||||
|
evaluator.enable = true;
|
||||||
|
queueRunner.enable = true;
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
database.url = "postgresql:///fc?host=/run/postgresql";
|
||||||
|
server = {
|
||||||
|
host = "127.0.0.1";
|
||||||
|
port = 3000;
|
||||||
|
cors_permissive = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
gc.enabled = false;
|
||||||
|
logs.log_dir = "/var/lib/fc/logs";
|
||||||
|
cache.enabled = true;
|
||||||
|
signing.enabled = false;
|
||||||
|
|
||||||
|
tracing = {
|
||||||
|
level = "info";
|
||||||
|
format = "compact";
|
||||||
|
show_targets = true;
|
||||||
|
show_timestamps = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
evaluator.poll_interval = 5;
|
||||||
|
evaluator.work_dir = "/var/lib/fc/evaluator";
|
||||||
|
queue_runner.poll_interval = 3;
|
||||||
|
queue_runner.work_dir = "/var/lib/fc/queue-runner";
|
||||||
|
|
||||||
|
# Declarative bootstrap: project + API key created on startup
|
||||||
|
declarative = {
|
||||||
|
projects = [
|
||||||
|
{
|
||||||
|
name = "declarative-project";
|
||||||
|
repository_url = "https://github.com/test/declarative";
|
||||||
|
description = "Bootstrap test project";
|
||||||
|
jobsets = [
|
||||||
|
{
|
||||||
|
name = "packages";
|
||||||
|
nix_expression = "packages";
|
||||||
|
enabled = true;
|
||||||
|
flake_mode = true;
|
||||||
|
check_interval = 300;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
api_keys = [
|
||||||
|
{
|
||||||
|
name = "bootstrap-admin";
|
||||||
|
key = "fc_bootstrap_key";
|
||||||
|
role = "admin";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue