* Put the project-related actions in a separate controller. Put the
actions for viewing the job status and all builds in a separate base class that's inherited both by Root.pm and Project.pm so that we get URIs like /jobstatus and /project/<name>/jobstatus for free.
This commit is contained in:
parent
2f1bcbb1e5
commit
0a40286202
38
src/Hydra/lib/Hydra/Base/Controller/ListBuilds.pm
Normal file
38
src/Hydra/lib/Hydra/Base/Controller/ListBuilds.pm
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package Hydra::Base::Controller::ListBuilds;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use base 'Catalyst::Controller';
|
||||||
|
use Hydra::Helper::Nix;
|
||||||
|
use Hydra::Helper::CatalystUtils;
|
||||||
|
|
||||||
|
|
||||||
|
sub jobstatus : Chained('get_builds') PathPart Args(0) {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
$c->stash->{latestBuilds} = getLatestBuilds($c, $c->stash->{allBuilds}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub all : Chained('get_builds') PathPart {
|
||||||
|
my ($self, $c, $page) = @_;
|
||||||
|
|
||||||
|
$c->stash->{template} = 'all.tt';
|
||||||
|
|
||||||
|
$page = (defined $page ? int($page) : 1) || 1;
|
||||||
|
|
||||||
|
my $resultsPerPage = 50;
|
||||||
|
|
||||||
|
my $nrBuilds = scalar($c->stash->{allBuilds}->search({finished => 1}));
|
||||||
|
|
||||||
|
$c->stash->{baseUri} = $c->uri_for($self->action_for("all"), $c->req->captures);
|
||||||
|
|
||||||
|
$c->stash->{page} = $page;
|
||||||
|
$c->stash->{resultsPerPage} = $resultsPerPage;
|
||||||
|
$c->stash->{totalBuilds} = $nrBuilds;
|
||||||
|
|
||||||
|
$c->stash->{builds} = [$c->stash->{allBuilds}->search(
|
||||||
|
{finished => 1}, {order_by => "timestamp DESC", rows => $resultsPerPage, page => $page})];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
1;
|
|
@ -2,7 +2,7 @@ package Hydra::Base::Controller::Nix;
|
||||||
|
|
||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings;
|
||||||
use parent 'Catalyst::Controller';
|
use base 'Catalyst::Controller';
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Hydra::Helper::CatalystUtils;
|
use Hydra::Helper::CatalystUtils;
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ sub runtimedeps : Chained('build') PathPart('runtime-deps') {
|
||||||
$c->stash->{current_view} = 'Hydra::View::NixDepGraph';
|
$c->stash->{current_view} = 'Hydra::View::NixDepGraph';
|
||||||
$c->stash->{storePaths} = [$build->outpath];
|
$c->stash->{storePaths} = [$build->outpath];
|
||||||
|
|
||||||
$c->response->content_type('image/png'); # !!!
|
$c->res->content_type('image/png'); # !!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ sub buildtimedeps : Chained('build') PathPart('buildtime-deps') {
|
||||||
$c->stash->{current_view} = 'Hydra::View::NixDepGraph';
|
$c->stash->{current_view} = 'Hydra::View::NixDepGraph';
|
||||||
$c->stash->{storePaths} = [$build->drvpath];
|
$c->stash->{storePaths} = [$build->drvpath];
|
||||||
|
|
||||||
$c->response->content_type('image/png'); # !!!
|
$c->res->content_type('image/png'); # !!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -183,7 +183,7 @@ sub restart : Chained('build') PathPart('restart') Args(0) {
|
||||||
|
|
||||||
$c->flash->{afterRestart} = "Build has been restarted.";
|
$c->flash->{afterRestart} = "Build has been restarted.";
|
||||||
|
|
||||||
$c->response->redirect($c->uri_for($self->action_for("view_build"), $c->req->captures));
|
$c->res->redirect($c->uri_for($self->action_for("view_build"), $c->req->captures));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
237
src/Hydra/lib/Hydra/Controller/Project.pm
Normal file
237
src/Hydra/lib/Hydra/Controller/Project.pm
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
package Hydra::Controller::Project;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use base 'Hydra::Base::Controller::ListBuilds';
|
||||||
|
use Hydra::Helper::Nix;
|
||||||
|
use Hydra::Helper::CatalystUtils;
|
||||||
|
|
||||||
|
|
||||||
|
__PACKAGE__->config->{namespace} = '';
|
||||||
|
|
||||||
|
|
||||||
|
sub project : Chained('/') PathPart('project') CaptureArgs(1) {
|
||||||
|
my ($self, $c, $projectName) = @_;
|
||||||
|
|
||||||
|
my $project = $c->model('DB::Projects')->find($projectName);
|
||||||
|
notFound($c, "Project $projectName doesn't exist.") unless defined $project;
|
||||||
|
|
||||||
|
$c->stash->{curProject} = $project;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub view : Chained('project') PathPart('') Args(0) {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
|
$c->stash->{template} = 'project.tt';
|
||||||
|
|
||||||
|
getBuildStats($c, scalar $c->stash->{curProject}->builds);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub edit : Chained('project') PathPart Args(0) {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
|
requireProjectOwner($c, $c->stash->{curProject});
|
||||||
|
|
||||||
|
$c->stash->{template} = 'project.tt';
|
||||||
|
$c->stash->{edit} = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub submit : Chained('project') PathPart Args(0) {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
|
requireProjectOwner($c, $c->stash->{curProject});
|
||||||
|
|
||||||
|
error($c, "Request must be POSTed.") if $c->request->method ne "POST";
|
||||||
|
|
||||||
|
$c->model('DB')->schema->txn_do(sub {
|
||||||
|
updateProject($c, $c->stash->{curProject});
|
||||||
|
});
|
||||||
|
|
||||||
|
$c->res->redirect($c->uri_for($self->action_for("view"), $c->req->captures));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub delete : Chained('project') PathPart Args(0) {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
|
requireProjectOwner($c, $c->stash->{curProject});
|
||||||
|
|
||||||
|
error($c, "Request must be POSTed.") if $c->request->method ne "POST";
|
||||||
|
|
||||||
|
$c->model('DB')->schema->txn_do(sub {
|
||||||
|
$c->stash->{curProject}->delete;
|
||||||
|
});
|
||||||
|
|
||||||
|
$c->res->redirect($c->uri_for("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub create : Path('create-project') {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
|
requireAdmin($c);
|
||||||
|
|
||||||
|
$c->stash->{template} = 'project.tt';
|
||||||
|
$c->stash->{create} = 1;
|
||||||
|
$c->stash->{edit} = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub create_submit : Path('create-project/submit') {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
|
||||||
|
requireAdmin($c);
|
||||||
|
|
||||||
|
my $projectName = trim $c->request->params->{name};
|
||||||
|
|
||||||
|
$c->model('DB')->schema->txn_do(sub {
|
||||||
|
# Note: $projectName is validated in updateProject,
|
||||||
|
# which will abort the transaction if the name isn't
|
||||||
|
# valid. Idem for the owner.
|
||||||
|
my $project = $c->model('DB::Projects')->create(
|
||||||
|
{name => $projectName, displayname => "", owner => trim $c->request->params->{owner}});
|
||||||
|
updateProject($c, $project);
|
||||||
|
});
|
||||||
|
|
||||||
|
$c->res->redirect($c->uri_for($self->action_for("view"), [$projectName]));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub updateProject {
|
||||||
|
my ($c, $project) = @_;
|
||||||
|
my $projectName = trim $c->request->params->{name};
|
||||||
|
error($c, "Invalid project name: " . ($projectName || "(empty)")) unless $projectName =~ /^[[:alpha:]]\w*$/;
|
||||||
|
|
||||||
|
my $displayName = trim $c->request->params->{displayname};
|
||||||
|
error($c, "Invalid display name: $displayName") if $displayName eq "";
|
||||||
|
|
||||||
|
$project->name($projectName);
|
||||||
|
$project->displayname($displayName);
|
||||||
|
$project->description(trim $c->request->params->{description});
|
||||||
|
$project->homepage(trim $c->request->params->{homepage});
|
||||||
|
$project->enabled(trim($c->request->params->{enabled}) eq "1" ? 1 : 0);
|
||||||
|
|
||||||
|
if ($c->check_user_roles('admin')) {
|
||||||
|
my $owner = trim $c->request->params->{owner};
|
||||||
|
error($c, "Invalid owner: $owner")
|
||||||
|
unless defined $c->model('DB::Users')->find({username => $owner});
|
||||||
|
$project->owner($owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
$project->update;
|
||||||
|
|
||||||
|
my %jobsetNames;
|
||||||
|
|
||||||
|
foreach my $param (keys %{$c->request->params}) {
|
||||||
|
next unless $param =~ /^jobset-(\w+)-name$/;
|
||||||
|
my $baseName = $1;
|
||||||
|
next if $baseName eq "template";
|
||||||
|
|
||||||
|
my $jobsetName = trim $c->request->params->{"jobset-$baseName-name"};
|
||||||
|
error($c, "Invalid jobset name: $jobsetName") unless $jobsetName =~ /^[[:alpha:]]\w*$/;
|
||||||
|
|
||||||
|
# The Nix expression path must be relative and can't contain ".." elements.
|
||||||
|
my $nixExprPath = trim $c->request->params->{"jobset-$baseName-nixexprpath"};
|
||||||
|
error($c, "Invalid Nix expression path: $nixExprPath") if $nixExprPath !~ /^$relPathRE$/;
|
||||||
|
|
||||||
|
my $nixExprInput = trim $c->request->params->{"jobset-$baseName-nixexprinput"};
|
||||||
|
error($c, "Invalid Nix expression input name: $nixExprInput") unless $nixExprInput =~ /^\w+$/;
|
||||||
|
|
||||||
|
$jobsetNames{$jobsetName} = 1;
|
||||||
|
|
||||||
|
my $jobset;
|
||||||
|
|
||||||
|
my $description = trim $c->request->params->{"jobset-$baseName-description"};
|
||||||
|
|
||||||
|
if ($baseName =~ /^\d+$/) { # numeric base name is auto-generated, i.e. a new entry
|
||||||
|
$jobset = $project->jobsets->create(
|
||||||
|
{ name => $jobsetName
|
||||||
|
, description => $description
|
||||||
|
, nixexprpath => $nixExprPath
|
||||||
|
, nixexprinput => $nixExprInput
|
||||||
|
});
|
||||||
|
} else { # it's an existing jobset
|
||||||
|
$jobset = ($project->jobsets->search({name => $baseName}))[0];
|
||||||
|
die unless defined $jobset;
|
||||||
|
$jobset->name($jobsetName);
|
||||||
|
$jobset->description($description);
|
||||||
|
$jobset->nixexprpath($nixExprPath);
|
||||||
|
$jobset->nixexprinput($nixExprInput);
|
||||||
|
$jobset->update;
|
||||||
|
}
|
||||||
|
|
||||||
|
my %inputNames;
|
||||||
|
|
||||||
|
# Process the inputs of this jobset.
|
||||||
|
foreach my $param (keys %{$c->request->params}) {
|
||||||
|
next unless $param =~ /^jobset-$baseName-input-(\w+)-name$/;
|
||||||
|
my $baseName2 = $1;
|
||||||
|
next if $baseName2 eq "template";
|
||||||
|
print STDERR "GOT INPUT: $baseName2\n";
|
||||||
|
|
||||||
|
my $inputName = trim $c->request->params->{"jobset-$baseName-input-$baseName2-name"};
|
||||||
|
error($c, "Invalid input name: $inputName") unless $inputName =~ /^[[:alpha:]]\w*$/;
|
||||||
|
|
||||||
|
my $inputType = trim $c->request->params->{"jobset-$baseName-input-$baseName2-type"};
|
||||||
|
error($c, "Invalid input type: $inputType") unless
|
||||||
|
$inputType eq "svn" || $inputType eq "cvs" || $inputType eq "tarball" ||
|
||||||
|
$inputType eq "string" || $inputType eq "path" || $inputType eq "boolean";
|
||||||
|
|
||||||
|
$inputNames{$inputName} = 1;
|
||||||
|
|
||||||
|
my $input;
|
||||||
|
if ($baseName2 =~ /^\d+$/) { # numeric base name is auto-generated, i.e. a new entry
|
||||||
|
$input = $jobset->jobsetinputs->create(
|
||||||
|
{ name => $inputName
|
||||||
|
, type => $inputType
|
||||||
|
});
|
||||||
|
} else { # it's an existing jobset
|
||||||
|
$input = ($jobset->jobsetinputs->search({name => $baseName2}))[0];
|
||||||
|
die unless defined $input;
|
||||||
|
$input->name($inputName);
|
||||||
|
$input->type($inputType);
|
||||||
|
$input->update;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the values for this input. Just delete all the
|
||||||
|
# current ones, then create the new values.
|
||||||
|
$input->jobsetinputalts->delete_all;
|
||||||
|
my $values = $c->request->params->{"jobset-$baseName-input-$baseName2-values"};
|
||||||
|
$values = [] unless defined $values;
|
||||||
|
$values = [$values] unless ref($values) eq 'ARRAY';
|
||||||
|
my $altnr = 0;
|
||||||
|
foreach my $value (@{$values}) {
|
||||||
|
print STDERR "VALUE: $value\n";
|
||||||
|
my $value = trim $value;
|
||||||
|
error($c, "Invalid Boolean value: $value") if
|
||||||
|
$inputType eq "boolean" && !($value eq "true" || $value eq "false");
|
||||||
|
$input->jobsetinputalts->create({altnr => $altnr++, value => $value});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get rid of deleted inputs.
|
||||||
|
my @inputs = $jobset->jobsetinputs->all;
|
||||||
|
foreach my $input (@inputs) {
|
||||||
|
$input->delete unless defined $inputNames{$input->name};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get rid of deleted jobsets, i.e., ones that are no longer submitted in the parameters.
|
||||||
|
my @jobsets = $project->jobsets->all;
|
||||||
|
foreach my $jobset (@jobsets) {
|
||||||
|
$jobset->delete unless defined $jobsetNames{$jobset->name};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Hydra::Base::Controller::ListBuilds needs this.
|
||||||
|
sub get_builds : Chained('project') PathPart('') CaptureArgs(0) {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
$c->stash->{allBuilds} = $c->stash->{curProject}->builds;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
1;
|
|
@ -3,6 +3,7 @@ package Hydra::Controller::Root;
|
||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings;
|
||||||
use base 'Hydra::Base::Controller::Nix';
|
use base 'Hydra::Base::Controller::Nix';
|
||||||
|
use base 'Hydra::Base::Controller::ListBuilds';
|
||||||
use Hydra::Helper::Nix;
|
use Hydra::Helper::Nix;
|
||||||
use Hydra::Helper::CatalystUtils;
|
use Hydra::Helper::CatalystUtils;
|
||||||
|
|
||||||
|
@ -18,34 +19,6 @@ sub begin :Private {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub trim {
|
|
||||||
my $s = shift;
|
|
||||||
$s =~ s/^\s+|\s+$//g;
|
|
||||||
return $s;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sub getBuildStats {
|
|
||||||
my ($c, $builds) = @_;
|
|
||||||
|
|
||||||
$c->stash->{finishedBuilds} = $builds->search({finished => 1}) || 0;
|
|
||||||
|
|
||||||
$c->stash->{succeededBuilds} = $builds->search(
|
|
||||||
{finished => 1, buildStatus => 0},
|
|
||||||
{join => 'resultInfo'}) || 0;
|
|
||||||
|
|
||||||
$c->stash->{scheduledBuilds} = $builds->search({finished => 0}) || 0;
|
|
||||||
|
|
||||||
$c->stash->{busyBuilds} = $builds->search(
|
|
||||||
{finished => 0, busy => 1},
|
|
||||||
{join => 'schedulingInfo'}) || 0;
|
|
||||||
|
|
||||||
$c->stash->{totalBuildTime} = $builds->search({},
|
|
||||||
{join => 'resultInfo', select => {sum => 'stoptime - starttime'}, as => ['sum']})
|
|
||||||
->first->get_column('sum') || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sub index :Path :Args(0) {
|
sub index :Path :Args(0) {
|
||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
$c->stash->{template} = 'index.tt';
|
$c->stash->{template} = 'index.tt';
|
||||||
|
@ -90,71 +63,6 @@ sub queue :Local {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Return the latest build for each job.
|
|
||||||
sub getLatestBuilds {
|
|
||||||
my ($c, $builds, $extraAttrs) = @_;
|
|
||||||
|
|
||||||
my @res = ();
|
|
||||||
|
|
||||||
foreach my $job ($builds->search({},
|
|
||||||
{group_by => ['project', 'attrname', 'system']}))
|
|
||||||
{
|
|
||||||
my $attrs =
|
|
||||||
{ project => $job->get_column('project')
|
|
||||||
, attrname => $job->attrname
|
|
||||||
, system => $job->system
|
|
||||||
, finished => 1
|
|
||||||
};
|
|
||||||
my ($build) = $builds->search({ %$attrs, %$extraAttrs },
|
|
||||||
{ join => 'resultInfo', order_by => 'timestamp DESC', rows => 1 } );
|
|
||||||
push @res, $build if defined $build;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [@res];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sub showJobStatus {
|
|
||||||
my ($c, $builds) = @_;
|
|
||||||
$c->stash->{template} = 'jobstatus.tt';
|
|
||||||
|
|
||||||
# Get the latest finished build for each unique job.
|
|
||||||
$c->stash->{latestBuilds} = getLatestBuilds($c, $builds, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sub jobstatus :Local {
|
|
||||||
my ($self, $c) = @_;
|
|
||||||
showJobStatus($c, $c->model('DB::Builds'));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sub showAllBuilds {
|
|
||||||
my ($c, $baseUri, $page, $builds) = @_;
|
|
||||||
$c->stash->{template} = 'all.tt';
|
|
||||||
|
|
||||||
$page = (defined $page ? int($page) : 1) || 1;
|
|
||||||
|
|
||||||
my $resultsPerPage = 50;
|
|
||||||
|
|
||||||
my $nrBuilds = scalar($builds->search({finished => 1}));
|
|
||||||
|
|
||||||
$c->stash->{baseUri} = $baseUri;
|
|
||||||
$c->stash->{page} = $page;
|
|
||||||
$c->stash->{resultsPerPage} = $resultsPerPage;
|
|
||||||
$c->stash->{totalBuilds} = $nrBuilds;
|
|
||||||
|
|
||||||
$c->stash->{builds} = [$builds->search(
|
|
||||||
{finished => 1}, {order_by => "timestamp DESC", rows => $resultsPerPage, page => $page})];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sub all :Local {
|
|
||||||
my ($self, $c, $page) = @_;
|
|
||||||
showAllBuilds($c, $c->uri_for("/all"), $page, $c->model('DB::Builds'));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sub releasesets :Local {
|
sub releasesets :Local {
|
||||||
my ($self, $c, $projectName) = @_;
|
my ($self, $c, $projectName) = @_;
|
||||||
$c->stash->{template} = 'releasesets.tt';
|
$c->stash->{template} = 'releasesets.tt';
|
||||||
|
@ -312,216 +220,6 @@ sub release :Local {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub updateProject {
|
|
||||||
my ($c, $project) = @_;
|
|
||||||
my $projectName = trim $c->request->params->{name};
|
|
||||||
die "Invalid project name: $projectName" unless $projectName =~ /^[[:alpha:]]\w*$/;
|
|
||||||
|
|
||||||
my $displayName = trim $c->request->params->{displayname};
|
|
||||||
die "Invalid display name: $displayName" if $displayName eq "";
|
|
||||||
|
|
||||||
$project->name($projectName);
|
|
||||||
$project->displayname($displayName);
|
|
||||||
$project->description(trim $c->request->params->{description});
|
|
||||||
$project->homepage(trim $c->request->params->{homepage});
|
|
||||||
$project->enabled(trim($c->request->params->{enabled}) eq "1" ? 1 : 0);
|
|
||||||
|
|
||||||
if ($c->check_user_roles('admin')) {
|
|
||||||
my $owner = trim $c->request->params->{owner};
|
|
||||||
die "Invalid owner: $owner"
|
|
||||||
unless defined $c->model('DB::Users')->find({username => $owner});
|
|
||||||
$project->owner($owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
$project->update;
|
|
||||||
|
|
||||||
my %jobsetNames;
|
|
||||||
|
|
||||||
foreach my $param (keys %{$c->request->params}) {
|
|
||||||
next unless $param =~ /^jobset-(\w+)-name$/;
|
|
||||||
my $baseName = $1;
|
|
||||||
next if $baseName eq "template";
|
|
||||||
|
|
||||||
my $jobsetName = trim $c->request->params->{"jobset-$baseName-name"};
|
|
||||||
die "Invalid jobset name: $jobsetName" unless $jobsetName =~ /^[[:alpha:]]\w*$/;
|
|
||||||
|
|
||||||
# The Nix expression path must be relative and can't contain ".." elements.
|
|
||||||
my $nixExprPath = trim $c->request->params->{"jobset-$baseName-nixexprpath"};
|
|
||||||
die "Invalid Nix expression path: $nixExprPath" if $nixExprPath !~ /^$relPathRE$/;
|
|
||||||
|
|
||||||
my $nixExprInput = trim $c->request->params->{"jobset-$baseName-nixexprinput"};
|
|
||||||
die "Invalid Nix expression input name: $nixExprInput" unless $nixExprInput =~ /^\w+$/;
|
|
||||||
|
|
||||||
$jobsetNames{$jobsetName} = 1;
|
|
||||||
|
|
||||||
my $jobset;
|
|
||||||
|
|
||||||
my $description = trim $c->request->params->{"jobset-$baseName-description"};
|
|
||||||
|
|
||||||
if ($baseName =~ /^\d+$/) { # numeric base name is auto-generated, i.e. a new entry
|
|
||||||
$jobset = $project->jobsets->create(
|
|
||||||
{ name => $jobsetName
|
|
||||||
, description => $description
|
|
||||||
, nixexprpath => $nixExprPath
|
|
||||||
, nixexprinput => $nixExprInput
|
|
||||||
});
|
|
||||||
} else { # it's an existing jobset
|
|
||||||
$jobset = ($project->jobsets->search({name => $baseName}))[0];
|
|
||||||
die unless defined $jobset;
|
|
||||||
$jobset->name($jobsetName);
|
|
||||||
$jobset->description($description);
|
|
||||||
$jobset->nixexprpath($nixExprPath);
|
|
||||||
$jobset->nixexprinput($nixExprInput);
|
|
||||||
$jobset->update;
|
|
||||||
}
|
|
||||||
|
|
||||||
my %inputNames;
|
|
||||||
|
|
||||||
# Process the inputs of this jobset.
|
|
||||||
foreach my $param (keys %{$c->request->params}) {
|
|
||||||
next unless $param =~ /^jobset-$baseName-input-(\w+)-name$/;
|
|
||||||
my $baseName2 = $1;
|
|
||||||
next if $baseName2 eq "template";
|
|
||||||
print STDERR "GOT INPUT: $baseName2\n";
|
|
||||||
|
|
||||||
my $inputName = trim $c->request->params->{"jobset-$baseName-input-$baseName2-name"};
|
|
||||||
die "Invalid input name: $inputName" unless $inputName =~ /^[[:alpha:]]\w*$/;
|
|
||||||
|
|
||||||
my $inputType = trim $c->request->params->{"jobset-$baseName-input-$baseName2-type"};
|
|
||||||
die "Invalid input type: $inputType" unless
|
|
||||||
$inputType eq "svn" || $inputType eq "cvs" || $inputType eq "tarball" ||
|
|
||||||
$inputType eq "string" || $inputType eq "path" || $inputType eq "boolean";
|
|
||||||
|
|
||||||
$inputNames{$inputName} = 1;
|
|
||||||
|
|
||||||
my $input;
|
|
||||||
if ($baseName2 =~ /^\d+$/) { # numeric base name is auto-generated, i.e. a new entry
|
|
||||||
$input = $jobset->jobsetinputs->create(
|
|
||||||
{ name => $inputName
|
|
||||||
, type => $inputType
|
|
||||||
});
|
|
||||||
} else { # it's an existing jobset
|
|
||||||
$input = ($jobset->jobsetinputs->search({name => $baseName2}))[0];
|
|
||||||
die unless defined $input;
|
|
||||||
$input->name($inputName);
|
|
||||||
$input->type($inputType);
|
|
||||||
$input->update;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update the values for this input. Just delete all the
|
|
||||||
# current ones, then create the new values.
|
|
||||||
$input->jobsetinputalts->delete_all;
|
|
||||||
my $values = $c->request->params->{"jobset-$baseName-input-$baseName2-values"};
|
|
||||||
$values = [] unless defined $values;
|
|
||||||
$values = [$values] unless ref($values) eq 'ARRAY';
|
|
||||||
my $altnr = 0;
|
|
||||||
foreach my $value (@{$values}) {
|
|
||||||
print STDERR "VALUE: $value\n";
|
|
||||||
my $value = trim $value;
|
|
||||||
die "Invalid Boolean value: $value" if
|
|
||||||
$inputType eq "boolean" && !($value eq "true" || $value eq "false");
|
|
||||||
$input->jobsetinputalts->create({altnr => $altnr++, value => $value});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get rid of deleted inputs.
|
|
||||||
my @inputs = $jobset->jobsetinputs->all;
|
|
||||||
foreach my $input (@inputs) {
|
|
||||||
$input->delete unless defined $inputNames{$input->name};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get rid of deleted jobsets, i.e., ones that are no longer submitted in the parameters.
|
|
||||||
my @jobsets = $project->jobsets->all;
|
|
||||||
foreach my $jobset (@jobsets) {
|
|
||||||
$jobset->delete unless defined $jobsetNames{$jobset->name};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sub project :Local {
|
|
||||||
my ($self, $c, $projectName, $subcommand, $arg) = @_;
|
|
||||||
$c->stash->{template} = 'project.tt';
|
|
||||||
|
|
||||||
my $project = $c->model('DB::Projects')->find($projectName);
|
|
||||||
notFound($c, "Project $projectName doesn't exist.") if !defined $project;
|
|
||||||
|
|
||||||
my $isPosted = $c->request->method eq "POST";
|
|
||||||
|
|
||||||
$c->stash->{curProject} = $project;
|
|
||||||
|
|
||||||
$subcommand = "" unless defined $subcommand;
|
|
||||||
|
|
||||||
if ($subcommand eq "jobstatus") {
|
|
||||||
return showJobStatus($c, scalar $project->builds);
|
|
||||||
}
|
|
||||||
|
|
||||||
elsif ($subcommand eq "all") {
|
|
||||||
return showAllBuilds($c, $c->uri_for("/project", $projectName, "all"),
|
|
||||||
$arg, scalar $project->builds);
|
|
||||||
}
|
|
||||||
|
|
||||||
elsif ($subcommand ne "") {
|
|
||||||
|
|
||||||
requireProjectOwner($c, $project);
|
|
||||||
|
|
||||||
if ($subcommand eq "edit") {
|
|
||||||
$c->stash->{edit} = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
elsif ($subcommand eq "submit" && $isPosted) {
|
|
||||||
$c->model('DB')->schema->txn_do(sub {
|
|
||||||
updateProject($c, $project);
|
|
||||||
});
|
|
||||||
return $c->res->redirect($c->uri_for("/project", $project->name));
|
|
||||||
}
|
|
||||||
|
|
||||||
elsif ($subcommand eq "delete" && $isPosted) {
|
|
||||||
$c->model('DB')->schema->txn_do(sub {
|
|
||||||
$project->delete;
|
|
||||||
});
|
|
||||||
return $c->res->redirect($c->uri_for("/"));
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
error($c, "Unknown subcommand $subcommand.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getBuildStats($c, scalar $project->builds);
|
|
||||||
|
|
||||||
$c->stash->{jobNames} =
|
|
||||||
[$c->model('DB::Builds')->search({project => $projectName}, {select => [{distinct => 'attrname'}], as => ['attrname']})];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sub createproject :Local {
|
|
||||||
my ($self, $c, $subcommand) = @_;
|
|
||||||
|
|
||||||
requireLogin($c) if !$c->user_exists;
|
|
||||||
|
|
||||||
error($c, "Only administrators can create projects.")
|
|
||||||
unless $c->check_user_roles('admin');
|
|
||||||
|
|
||||||
if (defined $subcommand && $subcommand eq "submit") {
|
|
||||||
my $projectName = trim $c->request->params->{name};
|
|
||||||
$c->model('DB')->schema->txn_do(sub {
|
|
||||||
# Note: $projectName is validated in updateProject,
|
|
||||||
# which will abort the transaction if the name isn't
|
|
||||||
# valid. Idem for the owner.
|
|
||||||
my $project = $c->model('DB::Projects')->create(
|
|
||||||
{name => $projectName, displayname => "", owner => trim $c->request->params->{owner}});
|
|
||||||
updateProject($c, $project);
|
|
||||||
});
|
|
||||||
return $c->res->redirect($c->uri_for("/project", $projectName));
|
|
||||||
}
|
|
||||||
|
|
||||||
$c->stash->{template} = 'project.tt';
|
|
||||||
$c->stash->{create} = 1;
|
|
||||||
$c->stash->{edit} = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sub job :Local {
|
sub job :Local {
|
||||||
my ($self, $c, $projectName, $jobName) = @_;
|
my ($self, $c, $projectName, $jobName) = @_;
|
||||||
$c->stash->{template} = 'job.tt';
|
$c->stash->{template} = 'job.tt';
|
||||||
|
@ -558,6 +256,13 @@ sub nix : Chained('/') PathPart('channel/latest') CaptureArgs(0) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Hydra::Base::Controller::ListBuilds needs this.
|
||||||
|
sub get_builds : Chained('/') PathPart('') CaptureArgs(0) {
|
||||||
|
my ($self, $c) = @_;
|
||||||
|
$c->stash->{allBuilds} = $c->model('DB::Builds');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
sub default :Path {
|
sub default :Path {
|
||||||
my ($self, $c) = @_;
|
my ($self, $c) = @_;
|
||||||
notFound($c, "Page not found.");
|
notFound($c, "Page not found.");
|
||||||
|
|
|
@ -6,8 +6,10 @@ use Readonly;
|
||||||
|
|
||||||
our @ISA = qw(Exporter);
|
our @ISA = qw(Exporter);
|
||||||
our @EXPORT = qw(
|
our @EXPORT = qw(
|
||||||
getBuild error notFound
|
getBuild getBuildStats getLatestBuilds
|
||||||
requireLogin requireProjectOwner
|
error notFound
|
||||||
|
requireLogin requireProjectOwner requireAdmin
|
||||||
|
trim
|
||||||
$pathCompRE $relPathRE
|
$pathCompRE $relPathRE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -19,6 +21,51 @@ sub getBuild {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub getBuildStats {
|
||||||
|
my ($c, $builds) = @_;
|
||||||
|
|
||||||
|
$c->stash->{finishedBuilds} = $builds->search({finished => 1}) || 0;
|
||||||
|
|
||||||
|
$c->stash->{succeededBuilds} = $builds->search(
|
||||||
|
{finished => 1, buildStatus => 0},
|
||||||
|
{join => 'resultInfo'}) || 0;
|
||||||
|
|
||||||
|
$c->stash->{scheduledBuilds} = $builds->search({finished => 0}) || 0;
|
||||||
|
|
||||||
|
$c->stash->{busyBuilds} = $builds->search(
|
||||||
|
{finished => 0, busy => 1},
|
||||||
|
{join => 'schedulingInfo'}) || 0;
|
||||||
|
|
||||||
|
$c->stash->{totalBuildTime} = $builds->search({},
|
||||||
|
{join => 'resultInfo', select => {sum => 'stoptime - starttime'}, as => ['sum']})
|
||||||
|
->first->get_column('sum') || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Return the latest build for each job.
|
||||||
|
sub getLatestBuilds {
|
||||||
|
my ($c, $builds, $extraAttrs) = @_;
|
||||||
|
|
||||||
|
my @res = ();
|
||||||
|
|
||||||
|
foreach my $job ($builds->search({},
|
||||||
|
{group_by => ['project', 'attrname', 'system']}))
|
||||||
|
{
|
||||||
|
my $attrs =
|
||||||
|
{ project => $job->get_column('project')
|
||||||
|
, attrname => $job->attrname
|
||||||
|
, system => $job->system
|
||||||
|
, finished => 1
|
||||||
|
};
|
||||||
|
my ($build) = $builds->search({ %$attrs, %$extraAttrs },
|
||||||
|
{ join => 'resultInfo', order_by => 'timestamp DESC', rows => 1 } );
|
||||||
|
push @res, $build if defined $build;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [@res];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
sub error {
|
sub error {
|
||||||
my ($c, $msg) = @_;
|
my ($c, $msg) = @_;
|
||||||
$c->error($msg);
|
$c->error($msg);
|
||||||
|
@ -46,11 +93,28 @@ sub requireProjectOwner {
|
||||||
|
|
||||||
requireLogin($c) if !$c->user_exists;
|
requireLogin($c) if !$c->user_exists;
|
||||||
|
|
||||||
error($c, "Only the project owner or the administrator can perform this operation.")
|
error($c, "Only the project owner or administrators can perform this operation.")
|
||||||
unless $c->check_user_roles('admin') || $c->user->username eq $project->owner->username;
|
unless $c->check_user_roles('admin') || $c->user->username eq $project->owner->username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub requireAdmin {
|
||||||
|
my ($c) = @_;
|
||||||
|
|
||||||
|
requireLogin($c) if !$c->user_exists;
|
||||||
|
|
||||||
|
error($c, "Only administrators can perform this operation.")
|
||||||
|
unless $c->check_user_roles('admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub trim {
|
||||||
|
my $s = shift;
|
||||||
|
$s =~ s/^\s+|\s+$//g;
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Security checking of filenames.
|
# Security checking of filenames.
|
||||||
Readonly::Scalar our $pathCompRE => "(?:[A-Za-z0-9-\+][A-Za-z0-9-\+\._]*)";
|
Readonly::Scalar our $pathCompRE => "(?:[A-Za-z0-9-\+][A-Za-z0-9-\+\._]*)";
|
||||||
Readonly::Scalar our $relPathRE => "(?:$pathCompRE(?:\/$pathCompRE)*)";
|
Readonly::Scalar our $relPathRE => "(?:$pathCompRE(?:\/$pathCompRE)*)";
|
||||||
|
|
|
@ -3,6 +3,12 @@
|
||||||
|
|
||||||
<h1>[% IF httpStatus %][% httpStatus %][% ELSE %]Error[% END %]</h1>
|
<h1>[% IF httpStatus %][% httpStatus %][% ELSE %]Error[% END %]</h1>
|
||||||
|
|
||||||
<p>I'm very sorry, but an error occurred: <span class="error-msg">[% FOREACH error IN errors %][% HTML.escape(error) %][% END %]</span></p>
|
<p>I'm very sorry, but the following error(s) occurred:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
[% FOREACH error IN errors %]
|
||||||
|
<li><div class="error-msg">[% HTML.escape(error) %]</div></li>
|
||||||
|
[% END %]
|
||||||
|
</ul>
|
||||||
|
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
|
@ -132,7 +132,7 @@
|
||||||
[% ELSE %]
|
[% ELSE %]
|
||||||
[% INCLUDE makeLink uri = c.uri_for('/login') title = "Login" %]
|
[% INCLUDE makeLink uri = c.uri_for('/login') title = "Login" %]
|
||||||
[% END %]
|
[% END %]
|
||||||
[% INCLUDE makeLink uri = c.uri_for('/createproject') title = "Create project" %]
|
[% INCLUDE makeLink uri = c.uri_for('/create-project') title = "Create project" %]
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -139,7 +139,7 @@
|
||||||
|
|
||||||
|
|
||||||
[% IF edit %]
|
[% IF edit %]
|
||||||
<form action="[% IF create %][% c.uri_for('/createproject/submit') %][% ELSE %][% c.uri_for('/project' curProject.name 'submit') %][% END %]" method="post">
|
<form action="[% IF create %][% c.uri_for('/create-project/submit') %][% ELSE %][% c.uri_for('/project' curProject.name 'submit') %][% END %]" method="post">
|
||||||
[% END %]
|
[% END %]
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue