488 lines
15 KiB
Perl
Raw Normal View History

2008-11-25 11:01:42 +00:00
package Hydra::Controller::Root;
2016-10-24 16:08:19 +02:00
use utf8;
use strict;
use warnings;
use base 'Hydra::Base::Controller::ListBuilds';
2008-11-25 11:01:42 +00:00
use Hydra::Helper::Nix;
use Hydra::Helper::CatalystUtils;
2010-12-03 09:40:25 +00:00
use Digest::SHA1 qw(sha1_hex);
use Nix::Store;
use Nix::Config;
use Encode;
use JSON;
# Put this controller at top-level.
__PACKAGE__->config->{namespace} = '';
sub noLoginNeeded {
my ($c) = @_;
return $c->request->path eq "google-login" ||
$c->request->path eq "login" ||
$c->request->path eq "logo" ||
$c->request->path =~ /^static\//;
}
2014-08-23 16:39:16 +02:00
2008-11-13 09:25:38 +00:00
sub begin :Private {
my ($self, $c, @args) = @_;
2008-11-13 09:48:10 +00:00
$c->stash->{curUri} = $c->request->uri;
$c->stash->{version} = $ENV{"HYDRA_RELEASE"} || "<devel>";
$c->stash->{nixVersion} = $ENV{"NIX_RELEASE"} || "<devel>";
$c->stash->{curTime} = time;
$c->stash->{logo} = defined $c->config->{hydra_logo} ? "/logo" : "";
2013-02-27 18:33:47 +01:00
$c->stash->{tracker} = $ENV{"HYDRA_TRACKER"};
$c->stash->{flashMsg} = $c->flash->{flashMsg};
$c->stash->{successMsg} = $c->flash->{successMsg};
$c->stash->{isPrivateHydra} = $c->config->{private} // "0" ne "0";
if ($c->stash->{isPrivateHydra} && ! noLoginNeeded($c)) {
requireUser($c);
}
2010-08-31 15:37:50 +00:00
if (scalar(@args) == 0 || $args[0] ne "static") {
$c->stash->{nrRunningBuilds} = dbh($c)->selectrow_array(
"select count(distinct build) from buildsteps where busy = 1");
2013-01-22 14:09:37 +01:00
$c->stash->{nrQueuedBuilds} = $c->model('DB::Builds')->search({ finished => 0 })->count();
}
# Gather the supported input types.
$c->stash->{inputTypes} = {
'string' => 'String value',
'boolean' => 'Boolean',
'nix' => 'Nix expression',
'build' => 'Previous Hydra build',
'sysbuild' => 'Previous Hydra build (same system)',
'eval' => 'Previous Hydra evaluation'
};
$_->supportedInputTypes($c->stash->{inputTypes}) foreach @{$c->hydra_plugins};
# XSRF protection: require POST requests to have the same origin.
if ($c->req->method eq "POST") {
my $referer = $c->req->header('Origin');
$referer //= $c->req->header('Referer');
my $base = $c->req->base;
2016-10-24 16:08:19 +02:00
error($c, "POST requests should come from $base.")
2016-10-21 17:56:34 +02:00
unless defined $referer && substr($referer, 0, length $base) eq $base;
}
$c->forward('deserialize');
$c->stash->{params} = $c->request->data or $c->request->params;
unless (defined $c->stash->{params} and %{$c->stash->{params}}) {
$c->stash->{params} = $c->request->params;
}
# Set the Vary header to "Accept" to ensure that browsers don't
# mix up HTML and JSON responses.
$c->response->headers->header('Vary', 'Accept');
2008-11-13 09:25:38 +00:00
}
sub deserialize :ActionClass('Deserialize') { }
2008-11-13 09:25:38 +00:00
sub index :Path :Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'overview.tt';
2010-06-04 14:43:28 +00:00
$c->stash->{projects} = [$c->model('DB::Projects')->search(isAdmin($c) ? {} : {hidden => 0}, {order_by => 'name'})];
$c->stash->{newsItems} = [$c->model('DB::NewsItems')->search({}, { order_by => ['createtime DESC'], rows => 5 })];
2013-10-03 14:50:56 +02:00
$self->status_ok($c,
entity => $c->stash->{projects}
);
}
sub queue :Local :Args(0) :ActionClass('REST') { }
sub queue_GET {
2008-11-26 19:48:04 +00:00
my ($self, $c) = @_;
$c->stash->{template} = 'queue.tt';
2013-02-27 18:33:47 +01:00
$c->stash->{flashMsg} //= $c->flash->{buildMsg};
$self->status_ok(
$c,
entity => [$c->model('DB::Builds')->search(
{ finished => 0 },
{ order_by => ["globalpriority desc", "id"],
, columns => [@buildListColumns]
})]
);
}
sub queue_summary :Local :Path('queue-summary') :Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'queue-summary.tt';
$c->stash->{queued} = dbh($c)->selectall_arrayref(
"select project, jobset, count(*) as queued, min(timestamp) as oldest, max(timestamp) as newest from Builds " .
"where finished = 0 group by project, jobset order by queued desc",
{ Slice => {} });
$c->stash->{systems} = dbh($c)->selectall_arrayref(
"select system, count(*) as c from Builds where finished = 0 group by system order by c desc",
{ Slice => {} });
}
sub status :Local :Args(0) :ActionClass('REST') { }
sub status_GET {
my ($self, $c) = @_;
$self->status_ok(
$c,
entity => [$c->model('DB::Builds')->search(
{ "buildsteps.busy" => 1 },
{ order_by => ["globalpriority DESC", "id"],
join => "buildsteps",
columns => [@buildListColumns]
})]
);
}
sub queue_runner_status :Local :Path('queue-runner-status') :Args(0) :ActionClass('REST') { }
sub queue_runner_status_GET {
my ($self, $c) = @_;
#my $status = from_json($c->model('DB::SystemStatus')->find('queue-runner')->status);
my $status = from_json(`hydra-queue-runner --status`);
if ($?) { $status->{status} = "unknown"; }
my $json = JSON->new->pretty()->canonical();
$c->stash->{template} = 'queue-runner-status.tt';
$c->stash->{status} = $json->encode($status);
$self->status_ok($c, entity => $status);
}
sub machines :Local Args(0) {
my ($self, $c) = @_;
my $machines = getMachines;
# Add entry for localhost.
$machines->{''} //= {};
delete $machines->{'localhost'};
my $status = $c->model('DB::SystemStatus')->find("queue-runner");
if ($status) {
my $ms = decode_json($status->status)->{"machines"};
foreach my $name (keys %{$ms}) {
$name = "" if $name eq "localhost";
$machines->{$name} //= {disabled => 1};
$machines->{$name}->{nrStepsDone} = $ms->{$name}->{nrStepsDone};
$machines->{$name}->{avgStepBuildTime} = $ms->{$name}->{avgStepBuildTime} // 0;
}
}
$c->stash->{machines} = $machines;
$c->stash->{steps} = dbh($c)->selectall_arrayref(
"select build, stepnr, s.system as system, s.drvpath as drvpath, machine, s.starttime as starttime, project, jobset, job " .
"from BuildSteps s join Builds b on s.build = b.id " .
"where busy = 1 order by machine, stepnr",
{ Slice => {} });
$c->stash->{template} = 'machine-status.tt';
}
# Hydra::Base::Controller::ListBuilds needs this.
sub get_builds : Chained('/') PathPart('') CaptureArgs(0) {
my ($self, $c) = @_;
$c->stash->{allBuilds} = $c->model('DB::Builds');
$c->stash->{latestSucceeded} = $c->model('DB')->resultset('LatestSucceeded');
$c->stash->{channelBaseName} = "everything";
$c->stash->{total} = $c->model('DB::NrBuilds')->find('finished')->count;
}
2009-03-31 13:48:03 +00:00
sub robots_txt : Path('robots.txt') {
my ($self, $c) = @_;
sub uri_for {
2013-01-23 12:41:57 +00:00
my ($c, $controller, $action, @args) = @_;
return $c->uri_for($c->controller($controller)->action_for($action), @args)->path;
}
sub channelUris {
2013-01-23 12:41:57 +00:00
my ($c, $controller, $bindings) = @_;
return
2013-01-23 12:41:57 +00:00
( uri_for($c, $controller, 'closure', $bindings, "*")
, uri_for($c, $controller, 'manifest', $bindings)
, uri_for($c, $controller, 'pkg', $bindings, "*")
, uri_for($c, $controller, 'nixexprs', $bindings)
, uri_for($c, $controller, 'channel_contents', $bindings)
);
}
2009-03-31 13:48:03 +00:00
# Put actions that are expensive or not useful for indexing in
# robots.txt. Note: wildcards are not universally supported in
# robots.txt, but apparently Google supports them.
my @rules =
2015-01-13 13:45:39 +01:00
( uri_for($c, 'Build', 'build', ["*"])
2013-01-23 12:41:57 +00:00
, uri_for($c, 'Root', 'nar', [], "*")
, uri_for($c, 'Root', 'status', [])
, uri_for($c, 'Root', 'all', [])
2015-01-13 13:50:13 +01:00
, uri_for($c, 'Root', 'queue', [])
2013-01-23 12:41:57 +00:00
, uri_for($c, 'API', 'scmdiff', [])
, uri_for($c, 'API', 'logdiff', [],"*", "*")
, uri_for($c, 'Project', 'all', ["*"])
2015-01-13 13:50:13 +01:00
, uri_for($c, 'Jobset', 'all', ["*", "*"])
, uri_for($c, 'Job', 'all', ["*", "*", "*"])
2013-01-23 12:41:57 +00:00
, channelUris($c, 'Root', ["*"])
, channelUris($c, 'Project', ["*", "*"])
, channelUris($c, 'Jobset', ["*", "*", "*"])
, channelUris($c, 'Job', ["*", "*", "*", "*"])
2009-03-31 13:48:03 +00:00
);
2009-03-31 14:55:47 +00:00
$c->stash->{'plain'} = { data => "User-agent: *\n" . join('', map { "Disallow: $_\n" } @rules) };
2009-03-31 13:48:03 +00:00
$c->forward('Hydra::View::Plain');
}
sub default :Path {
my ($self, $c) = @_;
2009-02-25 14:34:29 +00:00
notFound($c, "Page not found.");
}
sub end : ActionClass('RenderView') {
my ($self, $c) = @_;
if (defined $c->stash->{json}) {
2014-08-23 16:39:16 +02:00
if (scalar @{$c->error}) {
# FIXME: dunno why we need to do decode_utf8 here.
$c->stash->{json}->{error} = join "\n", map { decode_utf8($_); } @{$c->error};
$c->clear_errors;
}
$c->forward('View::JSON');
}
2013-10-03 14:45:23 +02:00
elsif (scalar @{$c->error}) {
$c->stash->{resource} = { error => join "\n", @{$c->error} };
$c->stash->{template} = 'error.tt';
2014-08-23 16:39:16 +02:00
$c->stash->{errors} = $c->error;
2013-02-22 14:27:38 +01:00
$c->response->status(500) if $c->response->status == 200;
2009-02-25 16:29:54 +00:00
if ($c->response->status >= 300) {
$c->stash->{httpStatus} =
$c->response->status . " " . HTTP::Status::status_message($c->response->status);
}
$c->clear_errors;
}
$c->forward('serialize') if defined $c->stash->{resource};
}
2013-10-03 14:45:23 +02:00
sub serialize : ActionClass('Serialize') { }
sub nar :Local :Args(1) {
my ($self, $c, $path) = @_;
die if $path =~ /\//;
my $storeMode = $c->config->{store_mode} // "direct";
if ($storeMode eq "s3-binary-cache") {
notFound($c, "There is no binary cache here.");
}
elsif ($storeMode eq "local-binary-cache") {
my $dir = $c->config->{binary_cache_dir};
$c->serve_static_file($dir . "/nar/" . $path);
}
else {
$path = $Nix::Config::storeDir . "/$path";
gone($c, "Path " . $path . " is no longer available.") unless isValidPath($path);
$c->stash->{current_view} = 'NixNAR';
$c->stash->{storePath} = $path;
}
}
sub nix_cache_info :Path('nix-cache-info') :Args(0) {
my ($self, $c) = @_;
my $storeMode = $c->config->{store_mode} // "direct";
if ($storeMode eq "s3-binary-cache") {
notFound($c, "There is no binary cache here.");
}
elsif ($storeMode eq "local-binary-cache") {
my $dir = $c->config->{binary_cache_dir};
$c->serve_static_file($dir . "/nix-cache-info");
}
else {
$c->response->content_type('text/plain');
$c->stash->{plain}->{data} =
"StoreDir: $Nix::Config::storeDir\n" .
"WantMassQuery: 0\n" .
# Give Hydra binary caches a very low priority (lower than the
# static binary cache http://nixos.org/binary-cache).
"Priority: 100\n";
setCacheHeaders($c, 24 * 60 * 60);
$c->forward('Hydra::View::Plain');
}
}
sub narinfo :LocalRegex('^([a-z0-9]+).narinfo$') :Args(0) {
my ($self, $c) = @_;
my $storeMode = $c->config->{store_mode} // "direct";
if ($storeMode eq "s3-binary-cache") {
notFound($c, "There is no binary cache here.");
}
elsif ($storeMode eq "local-binary-cache") {
my $dir = $c->config->{binary_cache_dir};
$c->serve_static_file($dir . "/" . $c->req->captures->[0] . ".narinfo");
}
else {
my $hash = $c->req->captures->[0];
die if length($hash) != 32;
my $path = queryPathFromHashPart($hash);
if (!$path) {
$c->response->status(404);
$c->response->content_type('text/plain');
$c->stash->{plain}->{data} = "does not exist\n";
$c->forward('Hydra::View::Plain');
setCacheHeaders($c, 60 * 60);
return;
}
$c->stash->{storePath} = $path;
$c->forward('Hydra::View::NARInfo');
}
}
sub logo :Local {
my ($self, $c) = @_;
my $path = $c->config->{hydra_logo} // die("Logo not set!");
$c->serve_static_file($path);
}
sub evals :Local Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'evals.tt';
my $page = int($c->req->param('page') || "1") || 1;
my $resultsPerPage = 20;
my $evals = $c->model('DB::JobsetEvals');
$c->stash->{page} = $page;
$c->stash->{resultsPerPage} = $resultsPerPage;
$c->stash->{total} = $evals->search({hasnewbuilds => 1})->count;
$c->stash->{evals} = getEvals($self, $c, $evals, ($page - 1) * $resultsPerPage, $resultsPerPage)
}
2015-07-10 15:08:34 +02:00
sub steps :Local Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'steps.tt';
my $page = int($c->req->param('page') || "1") || 1;
my $resultsPerPage = 20;
$c->stash->{page} = $page;
$c->stash->{resultsPerPage} = $resultsPerPage;
$c->stash->{steps} = [ $c->model('DB::BuildSteps')->search(
{ starttime => { '!=', undef },
stoptime => { '!=', undef }
},
{ order_by => [ "stoptime desc" ],
rows => $resultsPerPage,
offset => ($page - 1) * $resultsPerPage
}) ];
$c->stash->{total} = approxTableSize($c, "IndexBuildStepsOnStopTime");
}
sub search :Local Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'search.tt';
my $query = trim $c->request->params->{"query"};
error($c, "Query is empty.") if $query eq "";
error($c, "Invalid character in query.")
unless $query =~ /^[a-zA-Z0-9_\-\/.]+$/;
2013-02-22 16:41:42 +01:00
$c->stash->{limit} = 500;
$c->stash->{projects} = [ $c->model('DB::Projects')->search(
{ -and =>
[ { -or => [ name => { ilike => "%$query%" }, displayName => { ilike => "%$query%" }, description => { ilike => "%$query%" } ] }
, { hidden => 0 }
]
},
{ order_by => ["name"] } ) ];
$c->stash->{jobsets} = [ $c->model('DB::Jobsets')->search(
{ -and =>
[ { -or => [ "me.name" => { ilike => "%$query%" }, "me.description" => { ilike => "%$query%" } ] }
, { "project.hidden" => 0, "me.hidden" => 0 }
]
},
{ order_by => ["project", "name"], join => ["project"] } ) ];
$c->stash->{jobs} = [ $c->model('DB::Jobs')->search(
{ "me.name" => { ilike => "%$query%" }
, "project.hidden" => 0
, "jobset.hidden" => 0
},
{ order_by => ["enabled_ desc", "project", "jobset", "name"], join => ["project", "jobset"]
2013-04-01 20:18:00 -04:00
, "+select" => [\ "(project.enabled = 1 and jobset.enabled = 1 and exists (select 1 from Builds where project = project.name and jobset = jobset.name and job = me.name and iscurrent = 1)) as enabled_"]
, "+as" => ["enabled"]
2013-02-22 16:41:42 +01:00
, rows => $c->stash->{limit} + 1
} ) ];
# Perform build search in separate queries to prevent seq scan on buildoutputs table.
$c->stash->{builds} = [ $c->model('DB::Builds')->search(
{ "buildoutputs.path" => trim($query) },
{ order_by => ["id desc"], join => ["buildoutputs"] } ) ];
$c->stash->{buildsdrv} = [ $c->model('DB::Builds')->search(
{ "drvpath" => trim($query) },
{ order_by => ["id desc"] } ) ];
}
2015-07-10 15:08:34 +02:00
sub log :Local :Args(1) {
my ($self, $c, $path) = @_;
$path = ($ENV{NIX_STORE_DIR} || "/nix/store")."/$path";
my @outpaths = ($path);
my $logPath = findLog($c, $path, @outpaths);
notFound($c, "The build log of $path is not available.") unless defined $logPath;
$c->stash->{logPath} = $logPath;
$c->forward('Hydra::View::NixLog');
}
1;