External machines can now notify Hydra that it should check a repository by sending a GET or PUSH request to /api/push, providing a list of jobsets to be checked and/or a list of repository URLs. In the latter case, all jobsets that have any of the specified repositories as an input will be checked. For instance, you can configure GitHub or BitBucket to send a request to the URL http://hydra.example.org/api/push?repos=git://github.com/NixOS/nixpkgs.git to trigger evaluation of all jobsets that have git://github.com/NixOS/nixpkgs.git as an input, or to the URL http://hydra.example.org/api/push?jobsets=patchelf:trunk,nixpkgs:trunk to trigger evaluation of just the specified jobsets.
324 lines
9.3 KiB
Perl
324 lines
9.3 KiB
Perl
package Hydra::Controller::API;
|
||
|
||
use utf8;
|
||
use strict;
|
||
use warnings;
|
||
use base 'Catalyst::Controller';
|
||
use Hydra::Helper::Nix;
|
||
use Hydra::Helper::AddBuilds;
|
||
use Hydra::Helper::CatalystUtils;
|
||
use Hydra::Controller::Project;
|
||
use JSON::Any;
|
||
use DateTime;
|
||
use Digest::SHA qw(sha256_hex);
|
||
use Text::Diff;
|
||
use File::Slurp;
|
||
|
||
# !!! Rewrite this to use View::JSON.
|
||
|
||
|
||
sub api : Chained('/') PathPart('api') CaptureArgs(0) {
|
||
my ($self, $c) = @_;
|
||
$c->response->content_type('application/json');
|
||
}
|
||
|
||
|
||
sub end : ActionClass('RenderView') {
|
||
my ($self, $c) = @_;
|
||
if (scalar @{$c->error}) {
|
||
$c->stash->{json}->{error} = join "\n", @{$c->error};
|
||
$c->forward('View::JSON');
|
||
$c->clear_errors;
|
||
}
|
||
}
|
||
|
||
|
||
sub projectToHash {
|
||
my ($project) = @_;
|
||
return {
|
||
name => $project->name,
|
||
description => $project->description
|
||
};
|
||
}
|
||
|
||
|
||
sub projects : Chained('api') PathPart('projects') Args(0) {
|
||
my ($self, $c) = @_;
|
||
|
||
my @projects = $c->model('DB::Projects')->search({hidden => 0}, {order_by => 'name'});
|
||
|
||
my @list;
|
||
foreach my $p (@projects) {
|
||
push @list, projectToHash($p);
|
||
}
|
||
|
||
$c->stash->{'plain'} = {
|
||
data => scalar (JSON::Any->objToJson(\@list))
|
||
};
|
||
$c->forward('Hydra::View::Plain');
|
||
}
|
||
|
||
|
||
sub buildToHash {
|
||
my ($build) = @_;
|
||
my $result = {
|
||
id => $build->id,
|
||
project => $build->get_column("project"),
|
||
jobset => $build->get_column("jobset"),
|
||
job => $build->get_column("job"),
|
||
system => $build->system,
|
||
nixname => $build->nixname,
|
||
finished => $build->finished,
|
||
timestamp => $build->timestamp
|
||
};
|
||
|
||
if($build->finished) {
|
||
$result->{'buildstatus'} = $build->get_column("buildstatus");
|
||
} else {
|
||
$result->{'busy'} = $build->get_column("busy");
|
||
$result->{'priority'} = $build->get_column("priority");
|
||
}
|
||
|
||
return $result;
|
||
};
|
||
|
||
|
||
sub latestbuilds : Chained('api') PathPart('latestbuilds') Args(0) {
|
||
my ($self, $c) = @_;
|
||
my $nr = $c->request->params->{nr};
|
||
error($c, "Parameter not defined!") if !defined $nr;
|
||
|
||
my $project = $c->request->params->{project};
|
||
my $jobset = $c->request->params->{jobset};
|
||
my $job = $c->request->params->{job};
|
||
my $system = $c->request->params->{system};
|
||
|
||
my $filter = {finished => 1};
|
||
$filter->{project} = $project if ! $project eq "";
|
||
$filter->{jobset} = $jobset if ! $jobset eq "";
|
||
$filter->{job} = $job if !$job eq "";
|
||
$filter->{system} = $system if !$system eq "";
|
||
|
||
my @latest = $c->model('DB::Builds')->search($filter, {rows => $nr, order_by => ["timestamp DESC"] });
|
||
|
||
my @list;
|
||
push @list, buildToHash($_) foreach @latest;
|
||
|
||
$c->stash->{'plain'} = {
|
||
data => scalar (JSON::Any->objToJson(\@list))
|
||
};
|
||
$c->forward('Hydra::View::Plain');
|
||
}
|
||
|
||
|
||
sub jobsetToHash {
|
||
my ($jobset) = @_;
|
||
return {
|
||
project => $jobset->project->name,
|
||
name => $jobset->name,
|
||
nrscheduled => $jobset->get_column("nrscheduled"),
|
||
nrsucceeded => $jobset->get_column("nrsucceeded"),
|
||
nrfailed => $jobset->get_column("nrfailed"),
|
||
nrtotal => $jobset->get_column("nrtotal")
|
||
};
|
||
}
|
||
|
||
|
||
sub jobsets : Chained('api') PathPart('jobsets') Args(0) {
|
||
my ($self, $c) = @_;
|
||
|
||
my $projectName = $c->request->params->{project};
|
||
error($c, "Parameter 'project' not defined!") if !defined $projectName;
|
||
|
||
my $project = $c->model('DB::Projects')->find($projectName)
|
||
or notFound($c, "Project $projectName doesn't exist.");
|
||
|
||
my @jobsets = jobsetOverview($c, $project);
|
||
|
||
my @list;
|
||
push @list, jobsetToHash($_) foreach @jobsets;
|
||
|
||
$c->stash->{'plain'} = {
|
||
data => scalar (JSON::Any->objToJson(\@list))
|
||
};
|
||
$c->forward('Hydra::View::Plain');
|
||
}
|
||
|
||
|
||
sub queue : Chained('api') PathPart('queue') Args(0) {
|
||
my ($self, $c) = @_;
|
||
|
||
my $nr = $c->request->params->{nr};
|
||
error($c, "Parameter not defined!") if !defined $nr;
|
||
|
||
my @builds = $c->model('DB::Builds')->search({finished => 0}, {rows => $nr, order_by => ["busy DESC", "priority DESC", "timestamp"]});
|
||
|
||
my @list;
|
||
push @list, buildToHash($_) foreach @builds;
|
||
|
||
$c->stash->{'plain'} = {
|
||
data => scalar (JSON::Any->objToJson(\@list))
|
||
};
|
||
$c->forward('Hydra::View::Plain');
|
||
}
|
||
|
||
|
||
sub nrqueue : Chained('api') PathPart('nrqueue') Args(0) {
|
||
my ($self, $c) = @_;
|
||
my $nrQueuedBuilds = $c->model('DB::Builds')->search({finished => 0})->count();
|
||
$c->stash->{'plain'} = {
|
||
data => "$nrQueuedBuilds"
|
||
};
|
||
$c->forward('Hydra::View::Plain');
|
||
}
|
||
|
||
|
||
sub nrrunning : Chained('api') PathPart('nrrunning') Args(0) {
|
||
my ($self, $c) = @_;
|
||
my $nrRunningBuilds = $c->model('DB::Builds')->search({finished => 0, busy => 1 })->count();
|
||
$c->stash->{'plain'} = {
|
||
data => "$nrRunningBuilds"
|
||
};
|
||
$c->forward('Hydra::View::Plain');
|
||
}
|
||
|
||
|
||
sub nrbuilds : Chained('api') PathPart('nrbuilds') Args(0) {
|
||
my ($self, $c) = @_;
|
||
my $nr = $c->request->params->{nr};
|
||
my $period = $c->request->params->{period};
|
||
|
||
error($c, "Parameter not defined!") if !defined $nr || !defined $period;
|
||
my $base;
|
||
|
||
my $project = $c->request->params->{project};
|
||
my $jobset = $c->request->params->{jobset};
|
||
my $job = $c->request->params->{job};
|
||
my $system = $c->request->params->{system};
|
||
|
||
my $filter = {finished => 1};
|
||
$filter->{project} = $project if ! $project eq "";
|
||
$filter->{jobset} = $jobset if ! $jobset eq "";
|
||
$filter->{job} = $job if !$job eq "";
|
||
$filter->{system} = $system if !$system eq "";
|
||
|
||
$base = 60*60 if($period eq "hour");
|
||
$base = 24*60*60 if($period eq "day");
|
||
|
||
my @stats = $c->model('DB::Builds')->search($filter, {select => [{ count => "*" }], as => ["nr"], group_by => ["timestamp - timestamp % $base"], order_by => "timestamp - timestamp % $base DESC", rows => $nr});
|
||
my @arr;
|
||
push @arr, int($_->get_column("nr")) foreach @stats;
|
||
@arr = reverse(@arr);
|
||
|
||
$c->stash->{'plain'} = {
|
||
data => scalar (JSON::Any->objToJson(\@arr))
|
||
};
|
||
$c->forward('Hydra::View::Plain');
|
||
}
|
||
|
||
|
||
sub scmdiff : Chained('api') PathPart('scmdiff') Args(0) {
|
||
my ($self, $c) = @_;
|
||
|
||
my $uri = $c->request->params->{uri};
|
||
my $type = $c->request->params->{type};
|
||
my $rev1 = $c->request->params->{rev1};
|
||
my $rev2 = $c->request->params->{rev2};
|
||
my $branch;
|
||
|
||
die("invalid revisions: [$rev1] [$rev2]") if $rev1 !~ m/^[a-zA-Z0-9_.]+$/ || $rev2 !~ m/^[a-zA-Z0-9_.]+$/;
|
||
|
||
my $diff = "";
|
||
if ($type eq "hg") {
|
||
my $clonePath = scmPath . "/" . sha256_hex($uri);
|
||
die if ! -d $clonePath;
|
||
$branch = `(cd $clonePath; hg log --template '{branch}' -r $rev2)`;
|
||
$diff .= `(cd $clonePath; hg log -r $rev1 -r $rev2 -b $branch)`;
|
||
$diff .= `(cd $clonePath; hg diff -r $rev1:$rev2)`;
|
||
} elsif ($type eq "git") {
|
||
my $clonePath = scmPath . "/" . sha256_hex($uri);
|
||
die if ! -d $clonePath;
|
||
$diff .= `(cd $clonePath; git log $rev1..$rev2)`;
|
||
$diff .= `(cd $clonePath; git diff $rev1..$rev2)`;
|
||
}
|
||
|
||
$c->stash->{'plain'} = { data => (scalar $diff) || " " };
|
||
$c->forward('Hydra::View::Plain');
|
||
}
|
||
|
||
|
||
sub readNormalizedLog {
|
||
my ($file) = @_;
|
||
my $pipe = (-f "$file.bz2" ? "cat $file.bz2 | bzip2 -d" : "cat $file");
|
||
my $res = `$pipe`;
|
||
|
||
$res =~ s/\/nix\/store\/[a-z0-9]*-/\/nix\/store\/...-/g;
|
||
$res =~ s/nix-build-[a-z0-9]*-/nix-build-...-/g;
|
||
$res =~ s/[0-9]{2}:[0-9]{2}:[0-9]{2}/00:00:00/g;
|
||
return $res;
|
||
}
|
||
|
||
|
||
sub logdiff : Chained('api') PathPart('logdiff') Args(2) {
|
||
my ($self, $c, $buildid1, $buildid2) = @_;
|
||
|
||
my $diff = "";
|
||
|
||
my $build1 = getBuild($c, $buildid1);
|
||
notFound($c, "Build with ID $buildid1 doesn't exist.")
|
||
if !defined $build1;
|
||
my $build2 = getBuild($c, $buildid2);
|
||
notFound($c, "Build with ID $buildid2 doesn't exist.")
|
||
if !defined $build2;
|
||
|
||
if (-f $build1->logfile && -f $build2->logfile) {
|
||
my $logtext1 = readNormalizedLog($build1->logfile);
|
||
my $logtext2 = readNormalizedLog($build2->logfile);
|
||
$diff = diff \$logtext1, \$logtext2;
|
||
} else {
|
||
$c->response->status(404);
|
||
}
|
||
|
||
$c->response->content_type('text/x-diff');
|
||
$c->stash->{'plain'} = { data => (scalar $diff) || " " };
|
||
$c->forward('Hydra::View::Plain');
|
||
}
|
||
|
||
|
||
sub triggerJobset {
|
||
my ($self, $c, $jobset) = @_;
|
||
txn_do($c->model('DB')->schema, sub {
|
||
$jobset->update({ triggertime => time });
|
||
});
|
||
push @{$c->{stash}->{json}->{jobsetsTriggered}}, $jobset->project->name . ":" . $jobset->name;
|
||
}
|
||
|
||
|
||
sub push : Chained('api') PathPart('push') Args(0) {
|
||
my ($self, $c) = @_;
|
||
|
||
$c->{stash}->{json}->{jobsetsTriggered} = [];
|
||
|
||
my @jobsets = split /,/, ($c->request->params->{jobsets} // "");
|
||
foreach my $s (@jobsets) {
|
||
my ($p, $j) = parseJobsetName($s);
|
||
my $jobset = $c->model('DB::Jobsets')->find($p, $j) or notFound($c, "Jobset ‘$p:$j’ does not exist.");
|
||
next unless $jobset->project->enabled && $jobset->enabled;
|
||
triggerJobset($self, $c, $jobset);
|
||
}
|
||
|
||
my @repos = split /,/, ($c->request->params->{repos} // "");
|
||
foreach my $r (@repos) {
|
||
triggerJobset($self, $c, $_) foreach $c->model('DB::Jobsets')->search(
|
||
{ 'project.enabled' => 1, 'me.enabled' => 1 },
|
||
{ join => 'project'
|
||
, where => \ [ 'exists (select 1 from JobsetInputAlts where project = me.project and jobset = me.name and value = ?)', [ 'value', $r ] ]
|
||
});
|
||
}
|
||
|
||
$c->forward('View::JSON');
|
||
}
|
||
|
||
|
||
1;
|