RunCommand: Add a WIP execution of dynamic commands

This in-progress feature will run a dynamically generated set of
buildFinished hooks, which must be nested under the `runCommandHook.*`
attribute set. This implementation is not very good, with some to-dos:

1. Only run if the build succeeded
2. Verify the output is named $out and that it is an executable file
   (or a symlink to a file)
3. Require the jobset itself have a flag enabling the feature, since
   this feature can be a bit dangerous if various people of different
   trust levels can create the jobs.
This commit is contained in:
Graham Christensen 2021-12-08 16:03:43 -05:00
parent ea311a0eb4
commit e56c49333f
4 changed files with 161 additions and 47 deletions

View file

@ -65,10 +65,11 @@ sub eventMatches {
} }
sub fanoutToCommands { sub fanoutToCommands {
my ($config, $event, $project, $jobset, $job) = @_; my ($config, $event, $build) = @_;
my @commands; my @commands;
# Calculate all the statically defined commands to execute
my $cfg = $config->{runcommand}; my $cfg = $config->{runcommand};
my @config = defined $cfg ? ref $cfg eq "ARRAY" ? @$cfg : ($cfg) : (); my @config = defined $cfg ? ref $cfg eq "ARRAY" ? @$cfg : ($cfg) : ();
@ -77,9 +78,10 @@ sub fanoutToCommands {
next unless eventMatches($conf, $event); next unless eventMatches($conf, $event);
next unless configSectionMatches( next unless configSectionMatches(
$matcher, $matcher,
$project, $build->get_column('project'),
$jobset, $build->get_column('jobset'),
$job); $build->get_column('job')
);
if (!defined($conf->{command})) { if (!defined($conf->{command})) {
warn "<runcommand> section for '$matcher' lacks a 'command' option"; warn "<runcommand> section for '$matcher' lacks a 'command' option";
@ -92,6 +94,25 @@ sub fanoutToCommands {
}) })
} }
# Calculate all dynamically defined commands to execute
if (areDynamicCommandsEnabled($config)) {
# missing test cases:
#
# 1. is it enabled on the jobset?
# 2. what if the result is a directory?
# 3. what if the job doens't have an out?
# 4. what if the build failed?
my $job = $build->get_column('job');
if ($job =~ "^runCommandHook\.") {
my $out = $build->buildoutputs->find({name => "out"});
push(@commands, {
matcher => "DynamicRunCommand($job)",
command => $out->path
})
}
}
return \@commands; return \@commands;
} }
@ -160,9 +181,7 @@ sub buildFinished {
my $commandsToRun = fanoutToCommands( my $commandsToRun = fanoutToCommands(
$self->{config}, $self->{config},
$event, $event,
$build->project->get_column('name'), $build
$build->jobset->get_column('name'),
$build->get_column('job')
); );
if (@$commandsToRun == 0) { if (@$commandsToRun == 0) {

View file

@ -0,0 +1,108 @@
use strict;
use warnings;
use Setup;
my %ctx = test_init();
use Test2::V0;
use Hydra::Plugin::RunCommand;
require Hydra::Schema;
require Hydra::Model::DB;
use Test2::V0;
my $db = Hydra::Model::DB->new;
hydra_setup($db);
my $project = $db->resultset('Projects')->create({name => "tests", displayname => "", owner => "root"});
my $jobset = createBaseJobset("basic", "runcommand-dynamic.nix", $ctx{jobsdir});
ok(evalSucceeds($jobset), "Evaluating jobs/runcommand-dynamic.nix should exit with return code 0");
is(nrQueuedBuildsForJobset($jobset), 1, "Evaluating jobs/runcommand-dynamic.nix should result in 1 build1");
(my $build) = queuedBuildsForJobset($jobset);
is($build->job, "runCommandHook.example", "The only job should be runCommandHook.example");
ok(runBuild($build), "Build should exit with return code 0");
my $newbuild = $db->resultset('Builds')->find($build->id);
is($newbuild->finished, 1, "Build should be finished.");
is($newbuild->buildstatus, 0, "Build should have buildstatus 0.");
subtest "fanoutToCommands" => sub {
my $config = {
runcommand => [
{
job => "",
command => "foo"
},
{
job => "tests:*:*",
command => "bar"
},
{
job => "tests:basic:nomatch",
command => "baz"
}
]
};
is(
Hydra::Plugin::RunCommand::fanoutToCommands(
$config,
"buildFinished",
$newbuild
),
[
{
matcher => "",
command => "foo"
},
{
matcher => "tests:*:*",
command => "bar"
}
],
"fanoutToCommands returns a command per matching job"
);
};
subtest "fanoutToCommandsWithDynamicRunCommandSupport" => sub {
like(
$build->buildoutputs->find({name => "out"})->path,
qr/my-build-product$/,
"The way we find the out path is reasonable"
);
my $config = {
dynamicruncommand => { enable => 1 },
runcommand => [
{
job => "tests:basic:*",
command => "baz"
}
]
};
is(
Hydra::Plugin::RunCommand::fanoutToCommands(
$config,
"buildFinished",
$build
),
[
{
matcher => "tests:basic:*",
command => "baz"
},
{
matcher => "DynamicRunCommand(runCommandHook.example)",
command => $build->buildoutputs->find({name => "out"})->path
}
],
"fanoutToCommands returns a command per matching job"
);
};
done_testing;

View file

@ -249,44 +249,4 @@ subtest "eventMatches" => sub {
); );
}; };
subtest "fanoutToCommands" => sub {
my $config = {
runcommand => [
{
job => "",
command => "foo"
},
{
job => "project:*:*",
command => "bar"
},
{
job => "project:jobset:nomatch",
command => "baz"
}
]
};
is(
Hydra::Plugin::RunCommand::fanoutToCommands(
$config,
"buildFinished",
"project",
"jobset",
"job"
),
[
{
matcher => "",
command => "foo"
},
{
matcher => "project:*:*",
command => "bar"
}
],
"fanoutToCommands returns a command per matching job"
);
};
done_testing; done_testing;

View file

@ -0,0 +1,27 @@
with import ./config.nix;
{
runCommandHook.example = mkDerivation
{
name = "my-build-product";
builder = "/bin/sh";
outputs = [ "out" "bin" ];
args = [
(
builtins.toFile "builder.sh" ''
#! /bin/sh
echo "$PATH"
mkdir $bin
echo "foo" > $bin/bar
metrics=$out/nix-support/hydra-metrics
mkdir -p "$(dirname "$metrics")"
echo "lineCoverage 18 %" >> "$metrics"
echo "maxResident 27 KiB" >> "$metrics"
''
)
];
};
}