In nixpkgs this started to fail the hydra tests. It's not completely clear why because it seems the perlcritic rule has existed for quite some time. Anyway, this should solve the issues.
275 lines
7.5 KiB
Perl
275 lines
7.5 KiB
Perl
package Hydra::Plugin::RunCommand;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use parent 'Hydra::Plugin';
|
|
use experimental 'smartmatch';
|
|
use JSON::MaybeXS;
|
|
use File::Basename qw(dirname);
|
|
use File::Path qw(make_path);
|
|
use IPC::Run3;
|
|
use Try::Tiny;
|
|
|
|
sub isEnabled {
|
|
my ($self) = @_;
|
|
|
|
return areStaticCommandsEnabled($self->{config}) || areDynamicCommandsEnabled($self->{config});
|
|
}
|
|
|
|
sub areStaticCommandsEnabled {
|
|
my ($config) = @_;
|
|
|
|
if (defined $config->{runcommand}) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub areDynamicCommandsEnabled {
|
|
my ($config) = @_;
|
|
|
|
if ((defined $config->{dynamicruncommand})
|
|
&& $config->{dynamicruncommand}->{enable}) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub isBuildEligibleForDynamicRunCommand {
|
|
my ($build) = @_;
|
|
|
|
if ($build->get_column("buildstatus") != 0) {
|
|
return 0;
|
|
}
|
|
|
|
if ($build->get_column("job") =~ "^runCommandHook\..+") {
|
|
my $out = $build->buildoutputs->find({name => "out"});
|
|
if (!defined $out) {
|
|
warn "DynamicRunCommand hook on " . $build->job . " (" . $build->id . ") rejected: no output named 'out'.";
|
|
return 0;
|
|
}
|
|
|
|
my $path = $out->path;
|
|
if (-l $path) {
|
|
$path = readlink($path);
|
|
}
|
|
|
|
if (! -e $path) {
|
|
warn "DynamicRunCommand hook on " . $build->job . " (" . $build->id . ") rejected: The 'out' output doesn't exist locally. This is a bug.";
|
|
return 0;
|
|
}
|
|
|
|
if (! -x $path) {
|
|
warn "DynamicRunCommand hook on " . $build->job . " (" . $build->id . ") rejected: The 'out' output is not executable.";
|
|
return 0;
|
|
}
|
|
|
|
if (! -f $path) {
|
|
warn "DynamicRunCommand hook on " . $build->job . " (" . $build->id . ") rejected: The 'out' output is not a regular file or symlink.";
|
|
return 0;
|
|
}
|
|
|
|
if (! $build->jobset->supportsDynamicRunCommand()) {
|
|
warn "DynamicRunCommand hook on " . $build->job . " (" . $build->id . ") rejected: The project or jobset don't have dynamic runcommand enabled.";
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub configSectionMatches {
|
|
my ($name, $project, $jobset, $job) = @_;
|
|
|
|
my @elems = split /:/, $name;
|
|
|
|
die "invalid section name '$name'\n" if scalar(@elems) > 3;
|
|
|
|
my $project2 = $elems[0] // "*";
|
|
return 0 if $project2 ne "*" && $project ne $project2;
|
|
|
|
my $jobset2 = $elems[1] // "*";
|
|
return 0 if $jobset2 ne "*" && $jobset ne $jobset2;
|
|
|
|
my $job2 = $elems[2] // "*";
|
|
return 0 if $job2 ne "*" && $job ne $job2;
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub eventMatches {
|
|
my ($conf, $event) = @_;
|
|
for my $x (split " ", ($conf->{events} // "buildFinished")) {
|
|
return 1 if $x eq $event;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub fanoutToCommands {
|
|
my ($config, $event, $build) = @_;
|
|
|
|
my @commands;
|
|
|
|
# Calculate all the statically defined commands to execute
|
|
my $cfg = $config->{runcommand};
|
|
my @config = defined $cfg ? ref $cfg eq "ARRAY" ? @$cfg : ($cfg) : ();
|
|
|
|
foreach my $conf (@config) {
|
|
my $matcher = $conf->{job} // "*:*:*";
|
|
next unless eventMatches($conf, $event);
|
|
next unless configSectionMatches(
|
|
$matcher,
|
|
$build->jobset->get_column('project'),
|
|
$build->jobset->get_column('name'),
|
|
$build->get_column('job')
|
|
);
|
|
|
|
if (!defined($conf->{command})) {
|
|
warn "<runcommand> section for '$matcher' lacks a 'command' option";
|
|
next;
|
|
}
|
|
|
|
push(@commands, {
|
|
matcher => $matcher,
|
|
command => $conf->{command},
|
|
})
|
|
}
|
|
|
|
# Calculate all dynamically defined commands to execute
|
|
if (areDynamicCommandsEnabled($config)) {
|
|
if (isBuildEligibleForDynamicRunCommand($build)) {
|
|
my $job = $build->get_column('job');
|
|
my $out = $build->buildoutputs->find({name => "out"});
|
|
push(@commands, {
|
|
matcher => "DynamicRunCommand($job)",
|
|
command => $out->path
|
|
})
|
|
}
|
|
}
|
|
|
|
return \@commands;
|
|
}
|
|
|
|
sub makeJsonPayload {
|
|
my ($event, $build) = @_;
|
|
my $json = {
|
|
event => $event,
|
|
build => $build->id,
|
|
finished => $build->get_column('finished') ? JSON::MaybeXS::true : JSON::MaybeXS::false,
|
|
timestamp => $build->get_column('timestamp'),
|
|
project => $build->project->get_column('name'),
|
|
jobset => $build->jobset->get_column('name'),
|
|
job => $build->get_column('job'),
|
|
drvPath => $build->get_column('drvpath'),
|
|
startTime => $build->get_column('starttime'),
|
|
stopTime => $build->get_column('stoptime'),
|
|
buildStatus => $build->get_column('buildstatus'),
|
|
nixName => $build->get_column('nixname'),
|
|
system => $build->get_column('system'),
|
|
homepage => $build->get_column('homepage'),
|
|
description => $build->get_column('description'),
|
|
license => $build->get_column('license'),
|
|
outputs => [],
|
|
products => [],
|
|
metrics => [],
|
|
};
|
|
|
|
for my $output ($build->buildoutputs) {
|
|
my $j = {
|
|
name => $output->name,
|
|
path => $output->path,
|
|
};
|
|
push @{$json->{outputs}}, $j;
|
|
}
|
|
|
|
for my $product ($build->buildproducts) {
|
|
my $j = {
|
|
productNr => $product->productnr,
|
|
type => $product->type,
|
|
subtype => $product->subtype,
|
|
fileSize => $product->filesize,
|
|
sha256hash => $product->sha256hash,
|
|
path => $product->path,
|
|
name => $product->name,
|
|
defaultPath => $product->defaultpath,
|
|
};
|
|
push @{$json->{products}}, $j;
|
|
}
|
|
|
|
for my $metric ($build->buildmetrics) {
|
|
my $j = {
|
|
name => $metric->name,
|
|
unit => $metric->unit,
|
|
value => 0 + $metric->value,
|
|
};
|
|
push @{$json->{metrics}}, $j;
|
|
}
|
|
|
|
return $json;
|
|
}
|
|
|
|
sub buildFinished {
|
|
my ($self, $build, $dependents) = @_;
|
|
my $event = "buildFinished";
|
|
|
|
my $commandsToRun = fanoutToCommands(
|
|
$self->{config},
|
|
$event,
|
|
$build
|
|
);
|
|
|
|
if (@$commandsToRun == 0) {
|
|
# No matching jobs, don't bother generating the JSON
|
|
return;
|
|
}
|
|
|
|
my $tmp = File::Temp->new(SUFFIX => '.json');
|
|
print $tmp encode_json(makeJsonPayload($event, $build)) or die;
|
|
$ENV{"HYDRA_JSON"} = $tmp->filename;
|
|
|
|
foreach my $commandToRun (@{$commandsToRun}) {
|
|
my $command = $commandToRun->{command};
|
|
|
|
# todo: make all the to-run jobs "unstarted" in a batch, then start processing
|
|
my $runlog = $self->{db}->resultset("RunCommandLogs")->create({
|
|
job_matcher => $commandToRun->{matcher},
|
|
build_id => $build->get_column('id'),
|
|
command => $command
|
|
});
|
|
|
|
$runlog->started();
|
|
|
|
my $logPath = Hydra::Helper::Nix::constructRunCommandLogPath($runlog) or die "RunCommandLog not found.";
|
|
my $dir = dirname($logPath);
|
|
my $oldUmask = umask();
|
|
my $f;
|
|
|
|
try {
|
|
# file: 640, dir: 750
|
|
umask(0027);
|
|
make_path($dir);
|
|
|
|
open($f, '>', $logPath);
|
|
umask($oldUmask);
|
|
|
|
run3($command, \undef, $f, $f, { return_if_system_error => 1 }) == 1
|
|
or warn "notification command '$command' failed with exit status $? ($!)\n";
|
|
|
|
close($f);
|
|
|
|
$runlog->completed_with_child_error($?, $!);
|
|
1;
|
|
} catch {
|
|
die "Died while trying to process RunCommand (${\$runlog->uuid}): $_";
|
|
} finally {
|
|
umask($oldUmask);
|
|
};
|
|
}
|
|
}
|
|
|
|
1;
|