Merge in the first bits of the API work

The catalyst-action-rest branch from shlevy/hydra was an exploration of
using Catalyst::Action::REST to create a JSON API for hydra. This commit
merges in the best bits from that experiment, with the goal that further
API endpoints can be added incrementally.

In addition to migrating more endpoints, there is potential for
improvement in what's already been done:
* The web interface can be updated to use the same non-GET endpoints as
  the JSON interface (using x-tunneled-method) instead of having a
  separate endpoint
* The web rendering should use the $c->stash->{resource} data structure
  where applicable rather than putting the same data in two places in
  the stash
* Which columns to render for each endpoint is a completely debatable
  question
* Hydra::Component::ToJSON should turn has_many relations that have
  strings as their primary keys into objects instead of arrays

Fixes NixOS/hydra#98

Signed-off-by: Shea Levy <shea@shealevy.com>
This commit is contained in:
Shea Levy 2013-06-17 12:34:21 -04:00
parent d18fc4fc38
commit 002ac9ef63
52 changed files with 1163 additions and 272 deletions

View file

@ -71,6 +71,7 @@ in rec {
CatalystViewJSON
CatalystViewTT
CatalystXScriptServerStarman
CatalystActionREST
CryptRandPasswd
DBDPg
DBDSQLite
@ -167,4 +168,40 @@ in rec {
'';
});
tests.api = genAttrs' (system:
with import <nixos/lib/testing.nix> { inherit system; };
let hydra = builtins.getAttr system build; in # build.${system}
simpleTest {
machine =
{ config, pkgs, ... }:
{ services.postgresql.enable = true;
services.postgresql.package = pkgs.postgresql92;
environment.systemPackages = [ hydra pkgs.perlPackages.LWP pkgs.perlPackages.JSON ];
virtualisation.memorySize = 2048;
};
testScript =
''
$machine->waitForJob("postgresql");
# Initialise the database and the state.
$machine->mustSucceed
( "createdb -O root hydra"
, "psql hydra -f ${hydra}/libexec/hydra/sql/hydra-postgresql.sql"
, "mkdir /var/lib/hydra"
, "echo \"insert into Users(userName, emailAddress, password) values('root', 'e.dolstra\@tudelft.nl', '\$(echo -n foobar | sha1sum | cut -c1-40)');\" | psql hydra"
, "echo \"insert into UserRoles(userName, role) values('root', 'admin');\" | psql hydra"
, "mkdir /run/jobset"
, "chmod 755 /run/jobset"
, "cp ${./tests/api-test.nix} /run/jobset/default.nix"
, "chmod 644 /run/jobset/default.nix"
);
# Start the web interface.
$machine->mustSucceed("NIX_STORE_DIR=/run/nix NIX_LOG_DIR=/run/nix/var/log/nix NIX_STATE_DIR=/run/nix/var/nix HYDRA_DATA=/var/lib/hydra HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' LOGNAME=root DBIC_TRACE=1 hydra-server -d >&2 &");
$machine->waitForOpenPort("3000");
$machine->mustSucceed("perl ${./tests/api-test.pl} >&2");
'';
});
}

View file

@ -101,7 +101,7 @@ sub latest : Chained('get_builds') PathPart('latest') {
notFound($c, "There is no successful build to redirect to.") unless defined $latest;
$c->res->redirect($c->uri_for($c->controller('Build')->action_for("view_build"), [$latest->id], @rest));
$c->res->redirect($c->uri_for($c->controller('Build')->action_for("build"), [$latest->id], @rest));
}
@ -116,7 +116,7 @@ sub latest_for : Chained('get_builds') PathPart('latest-for') {
notFound($c, "There is no successful build for platform `$system' to redirect to.") unless defined $latest;
$c->res->redirect($c->uri_for($c->controller('Build')->action_for("view_build"), [$latest->id], @rest));
$c->res->redirect($c->uri_for($c->controller('Build')->action_for("build"), [$latest->id], @rest));
}

View file

@ -2,7 +2,7 @@ package Hydra::Base::Controller::NixChannel;
use strict;
use warnings;
use base 'Catalyst::Controller';
use base 'Hydra::Base::Controller::REST';
use List::MoreUtils qw(all);
use Nix::Store;
use Hydra::Helper::Nix;

View file

@ -0,0 +1,18 @@
package Hydra::Base::Controller::REST;
use strict;
use warnings;
use base 'Catalyst::Controller::REST';
__PACKAGE__->config(
map => {
'text/html' => [ 'View', 'TT' ]
},
default => 'text/html',
'stash_key' => 'resource',
);
sub begin { my ( $self, $c ) = @_; $c->forward('Hydra::Controller::Root::begin'); }
sub end { my ( $self, $c ) = @_; $c->forward('Hydra::Controller::Root::end'); }
1;

View file

@ -0,0 +1,43 @@
use utf8;
package Hydra::Component::ToJSON;
use strict;
use warnings;
use base 'DBIx::Class';
sub TO_JSON {
my $self = shift;
my $json = { $self->get_columns };
my $rs = $self->result_source;
my @relnames = $rs->relationships;
RELLOOP: foreach my $relname (@relnames) {
my $relinfo = $rs->relationship_info($relname);
next unless defined $relinfo->{attrs}->{accessor};
my $accessor = $relinfo->{attrs}->{accessor};
if ($accessor eq "single" and exists $self->{_relationship_data}{$relname}) {
$json->{$relname} = $self->$relname->TO_JSON;
} else {
unless (defined $self->{related_resultsets}{$relname}) {
my $cond = $relinfo->{cond};
if (ref $cond eq 'HASH') {
foreach my $k (keys %{$cond}) {
my $v = $cond->{$k};
$v =~ s/^self\.//;
next RELLOOP unless $self->has_column_loaded($v);
}
} #!!! TODO: Handle ARRAY conditions
}
if (defined $self->related_resultset($relname)->get_cache) {
if ($accessor eq "multi") {
$json->{$relname} = [ map { $_->TO_JSON } $self->$relname ];
} else {
$json->{$relname} = $self->$relname->TO_JSON;
}
}
}
}
return $json;
}
1;

View file

@ -3,7 +3,7 @@ package Hydra::Controller::API;
use utf8;
use strict;
use warnings;
use base 'Catalyst::Controller';
use base 'Hydra::Base::Controller::REST';
use Hydra::Helper::Nix;
use Hydra::Helper::AddBuilds;
use Hydra::Helper::CatalystUtils;
@ -310,6 +310,11 @@ sub push : Chained('api') PathPart('push') Args(0) {
, where => \ [ 'exists (select 1 from JobsetInputAlts where project = me.project and jobset = me.name and value = ?)', [ 'value', $r ] ]
});
}
$self->status_ok(
$c,
entity => { jobsetsTriggered => $c->stash->{json}->{jobsetsTriggered} }
);
}

View file

@ -14,7 +14,7 @@ use Nix::Config;
use List::MoreUtils qw(all);
sub build : Chained('/') PathPart CaptureArgs(1) {
sub buildChain :Chained('/') :PathPart('build') :CaptureArgs(1) {
my ($self, $c, $id) = @_;
$c->stash->{id} = $id;
@ -50,7 +50,9 @@ sub findBuildStepByDrvPath {
}
sub view_build : Chained('build') PathPart('') Args(0) {
sub build :Chained('buildChain') :PathPart('') :Args(0) :ActionClass('REST') { }
sub build_GET {
my ($self, $c) = @_;
my $build = $c->stash->{build};
@ -96,10 +98,26 @@ sub view_build : Chained('build') PathPart('') Args(0) {
($c->stash->{eval}) = $c->stash->{build}->jobsetevals->search(
{ hasnewbuilds => 1},
{ limit => 1, order_by => ["id"] });
$self->status_ok(
$c,
entity => $c->model('DB::Builds')->find($build->id,{
columns => [
'id',
'finished',
'timestamp',
'buildstatus',
'job',
'project',
'jobset',
'starttime',
'stoptime',
]
})
);
}
sub view_nixlog : Chained('build') PathPart('nixlog') {
sub view_nixlog : Chained('buildChain') PathPart('nixlog') {
my ($self, $c, $stepnr, $mode) = @_;
my $step = $c->stash->{build}->buildsteps->find({stepnr => $stepnr});
@ -111,7 +129,7 @@ sub view_nixlog : Chained('build') PathPart('nixlog') {
}
sub view_log : Chained('build') PathPart('log') {
sub view_log : Chained('buildChain') PathPart('log') {
my ($self, $c, $mode) = @_;
showLog($c, $c->stash->{build}->drvpath, $mode);
}
@ -176,7 +194,7 @@ sub checkPath {
}
sub download : Chained('build') PathPart {
sub download : Chained('buildChain') PathPart {
my ($self, $c, $productnr, @path) = @_;
$productnr = 1 if !defined $productnr;
@ -223,7 +241,7 @@ sub download : Chained('build') PathPart {
# Redirect to a download with the given type. Useful when you want to
# link to some build product of the latest build (i.e. in conjunction
# with the .../latest redirect).
sub download_by_type : Chained('build') PathPart('download-by-type') {
sub download_by_type : Chained('buildChain') PathPart('download-by-type') {
my ($self, $c, $type, $subtype, @path) = @_;
notFound($c, "You need to specify a type and a subtype in the URI.")
@ -238,7 +256,7 @@ sub download_by_type : Chained('build') PathPart('download-by-type') {
}
sub contents : Chained('build') PathPart Args(1) {
sub contents : Chained('buildChain') PathPart Args(1) {
my ($self, $c, $productnr) = @_;
my $product = $c->stash->{build}->buildproducts->find({productnr => $productnr});
@ -342,7 +360,7 @@ sub getDependencyGraph {
}
sub build_deps : Chained('build') PathPart('build-deps') {
sub build_deps : Chained('buildChain') PathPart('build-deps') {
my ($self, $c) = @_;
my $build = $c->stash->{build};
my $drvPath = $build->drvpath;
@ -355,7 +373,7 @@ sub build_deps : Chained('build') PathPart('build-deps') {
}
sub runtime_deps : Chained('build') PathPart('runtime-deps') {
sub runtime_deps : Chained('buildChain') PathPart('runtime-deps') {
my ($self, $c) = @_;
my $build = $c->stash->{build};
my @outPaths = map { $_->path } $build->buildoutputs->all;
@ -369,7 +387,7 @@ sub runtime_deps : Chained('build') PathPart('runtime-deps') {
}
sub nix : Chained('build') PathPart('nix') CaptureArgs(0) {
sub nix : Chained('buildChain') PathPart('nix') CaptureArgs(0) {
my ($self, $c) = @_;
my $build = $c->stash->{build};
@ -389,7 +407,7 @@ sub nix : Chained('build') PathPart('nix') CaptureArgs(0) {
}
sub restart : Chained('build') PathPart Args(0) {
sub restart : Chained('buildChain') PathPart Args(0) {
my ($self, $c) = @_;
my $build = $c->stash->{build};
@ -404,11 +422,11 @@ sub restart : Chained('build') PathPart Args(0) {
$c->flash->{buildMsg} = "Build has been restarted.";
$c->res->redirect($c->uri_for($self->action_for("view_build"), $c->req->captures));
$c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures));
}
sub cancel : Chained('build') PathPart Args(0) {
sub cancel : Chained('buildChain') PathPart Args(0) {
my ($self, $c) = @_;
my $build = $c->stash->{build};
@ -434,11 +452,11 @@ sub cancel : Chained('build') PathPart Args(0) {
$c->flash->{buildMsg} = "Build has been cancelled.";
$c->res->redirect($c->uri_for($self->action_for("view_build"), $c->req->captures));
$c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures));
}
sub keep : Chained('build') PathPart Args(1) {
sub keep : Chained('buildChain') PathPart Args(1) {
my ($self, $c, $x) = @_;
my $keep = $x eq "1" ? 1 : 0;
@ -457,11 +475,11 @@ sub keep : Chained('build') PathPart Args(1) {
$c->flash->{buildMsg} =
$keep ? "Build will be kept." : "Build will not be kept.";
$c->res->redirect($c->uri_for($self->action_for("view_build"), $c->req->captures));
$c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures));
}
sub add_to_release : Chained('build') PathPart('add-to-release') Args(0) {
sub add_to_release : Chained('buildChain') PathPart('add-to-release') Args(0) {
my ($self, $c) = @_;
my $build = $c->stash->{build};
@ -486,11 +504,11 @@ sub add_to_release : Chained('build') PathPart('add-to-release') Args(0) {
$c->flash->{buildMsg} = "Build added to project <tt>$releaseName</tt>.";
$c->res->redirect($c->uri_for($self->action_for("view_build"), $c->req->captures));
$c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures));
}
sub clone : Chained('build') PathPart('clone') Args(0) {
sub clone : Chained('buildChain') PathPart('clone') Args(0) {
my ($self, $c) = @_;
my $build = $c->stash->{build};
@ -501,7 +519,7 @@ sub clone : Chained('build') PathPart('clone') Args(0) {
}
sub clone_submit : Chained('build') PathPart('clone/submit') Args(0) {
sub clone_submit : Chained('buildChain') PathPart('clone/submit') Args(0) {
my ($self, $c) = @_;
my $build = $c->stash->{build};
@ -567,7 +585,7 @@ sub clone_submit : Chained('build') PathPart('clone/submit') Args(0) {
}
sub get_info : Chained('build') PathPart('api/get-info') Args(0) {
sub get_info : Chained('buildChain') PathPart('api/get-info') Args(0) {
my ($self, $c) = @_;
my $build = $c->stash->{build};
$c->stash->{json}->{buildId} = $build->id;
@ -578,7 +596,7 @@ sub get_info : Chained('build') PathPart('api/get-info') Args(0) {
}
sub evals : Chained('build') PathPart('evals') Args(0) {
sub evals : Chained('buildChain') PathPart('evals') Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'evals.tt';
@ -596,7 +614,7 @@ sub evals : Chained('build') PathPart('evals') Args(0) {
}
sub reproduce : Chained('build') PathPart('reproduce') Args(0) {
sub reproduce : Chained('buildChain') PathPart('reproduce') Args(0) {
my ($self, $c) = @_;
$c->response->content_type('text/x-shellscript');
$c->response->header('Content-Disposition', 'attachment; filename="reproduce-build-' . $c->stash->{build}->id . '.sh"');

View file

@ -7,35 +7,143 @@ use Hydra::Helper::Nix;
use Hydra::Helper::CatalystUtils;
sub jobset : Chained('/') PathPart('jobset') CaptureArgs(2) {
sub jobsetChain :Chained('/') :PathPart('jobset') :CaptureArgs(2) {
my ($self, $c, $projectName, $jobsetName) = @_;
my $project = $c->model('DB::Projects')->find($projectName)
or notFound($c, "Project $projectName doesn't exist.");
my $project = $c->model('DB::Projects')->find($projectName);
if ($project) {
$c->stash->{project} = $project;
$c->stash->{jobset_} = $project->jobsets->search({name => $jobsetName});
$c->stash->{jobset} = $c->stash->{jobset_}->single
or notFound($c, "Jobset $jobsetName doesn't exist.");
$c->stash->{jobset_} = $project->jobsets->search({'me.name' => $jobsetName});
my $jobset = $c->stash->{jobset_}->single;
if ($jobset) {
$c->stash->{jobset} = $jobset;
} else {
if ($c->action->name eq "jobset" and $c->request->method eq "PUT") {
$c->stash->{jobsetName} = $jobsetName;
} else {
$self->status_not_found(
$c,
message => "Jobset $jobsetName doesn't exist."
);
$c->detach;
}
}
} else {
$self->status_not_found(
$c,
message => "Project $projectName doesn't exist."
);
$c->detach;
}
}
sub index : Chained('jobset') PathPart('') Args(0) {
sub jobset :Chained('jobsetChain') :PathPart('') :Args(0) :ActionClass('REST::ForBrowsers') { }
sub jobset_GET {
my ($self, $c) = @_;
$c->stash->{template} = 'jobset.tt';
my $projectName = $c->stash->{project}->name;
my $jobsetName = $c->stash->{jobset}->name;
$c->stash->{evals} = getEvals($self, $c, scalar $c->stash->{jobset}->jobsetevals, 0, 10);
($c->stash->{latestEval}) = $c->stash->{jobset}->jobsetevals->search({}, { limit => 1, order_by => ["id desc"] });
$self->status_ok(
$c,
entity => $c->stash->{jobset_}->find({}, {
columns => [
'me.name',
'me.project',
'me.errormsg',
'jobsetinputs.name',
{
'jobsetinputs.jobsetinputalts.altnr' => 'jobsetinputalts.altnr',
'jobsetinputs.jobsetinputalts.value' => 'jobsetinputalts.value'
}
],
join => { 'jobsetinputs' => 'jobsetinputalts' },
collapse => 1,
order_by => "me.name"
})
);
}
sub jobset_PUT {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
if (defined $c->stash->{jobset}) {
error($c, "Cannot rename jobset `$c->stash->{params}->{oldName}' over existing jobset `$c->stash->{jobset}->name") if defined $c->stash->{params}->{oldName} and $c->stash->{params}->{oldName} ne $c->stash->{jobset}->name;
txn_do($c->model('DB')->schema, sub {
updateJobset($c, $c->stash->{jobset});
});
if ($c->req->looks_like_browser) {
$c->res->redirect($c->uri_for($self->action_for("jobset"),
[$c->stash->{project}->name, $c->stash->{jobset}->name]) . "#tabs-configuration");
} else {
$self->status_no_content($c);
}
} elsif (defined $c->stash->{params}->{oldName}) {
my $jobset = $c->stash->{project}->jobsets->find({'me.name' => $c->stash->{params}->{oldName}});
if (defined $jobset) {
txn_do($c->model('DB')->schema, sub {
updateJobset($c, $jobset);
});
my $uri = $c->uri_for($self->action_for("jobset"), [$c->stash->{project}->name, $jobset->name]);
if ($c->req->looks_like_browser) {
$c->res->redirect($uri . "#tabs-configuration");
} else {
$self->status_created(
$c,
location => "$uri",
entity => { name => $jobset->name, uri => "$uri", type => "jobset" }
);
}
} else {
$self->status_not_found(
$c,
message => "Jobset $c->stash->{params}->{oldName} doesn't exist."
);
}
} else {
my $exprType =
$c->stash->{params}->{"nixexprpath"} =~ /.scm$/ ? "guile" : "nix";
error($c, "Invalid jobset name: $c->stash->{jobsetName}") if $c->stash->{jobsetName} !~ /^$jobsetNameRE$/;
my $jobset;
txn_do($c->model('DB')->schema, sub {
# Note: $jobsetName is validated in updateProject, which will
# abort the transaction if the name isn't valid.
$jobset = $c->stash->{project}->jobsets->create(
{name => $c->stash->{jobsetName}, nixexprinput => "", nixexprpath => "", emailoverride => ""});
updateJobset($c, $jobset);
});
my $uri = $c->uri_for($self->action_for("jobset"), [$c->stash->{project}->name, $jobset->name]);
if ($c->req->looks_like_browser) {
$c->res->redirect($uri . "#tabs-configuration");
} else {
$self->status_created(
$c,
location => "$uri",
entity => { name => $jobset->name, uri => "$uri", type => "jobset" }
);
}
}
}
sub jobs_tab : Chained('jobset') PathPart('jobs-tab') Args(0) {
sub jobs_tab : Chained('jobsetChain') PathPart('jobs-tab') Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'jobset-jobs-tab.tt';
@ -64,7 +172,7 @@ sub jobs_tab : Chained('jobset') PathPart('jobs-tab') Args(0) {
}
sub status_tab : Chained('jobset') PathPart('status-tab') Args(0) {
sub status_tab : Chained('jobsetChain') PathPart('status-tab') Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'jobset-status-tab.tt';
@ -101,7 +209,7 @@ sub status_tab : Chained('jobset') PathPart('status-tab') Args(0) {
# Hydra::Base::Controller::ListBuilds needs this.
sub get_builds : Chained('jobset') PathPart('') CaptureArgs(0) {
sub get_builds : Chained('jobsetChain') PathPart('') CaptureArgs(0) {
my ($self, $c) = @_;
$c->stash->{allBuilds} = $c->stash->{jobset}->builds;
$c->stash->{jobStatus} = $c->model('DB')->resultset('JobStatusForJobset')
@ -115,7 +223,7 @@ sub get_builds : Chained('jobset') PathPart('') CaptureArgs(0) {
}
sub edit : Chained('jobset') PathPart Args(0) {
sub edit : Chained('jobsetChain') PathPart Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
@ -125,10 +233,9 @@ sub edit : Chained('jobset') PathPart Args(0) {
}
sub submit : Chained('jobset') PathPart Args(0) {
sub submit : Chained('jobsetChain') PathPart Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
requirePost($c);
if (($c->request->params->{submit} // "") eq "delete") {
@ -137,15 +244,17 @@ sub submit : Chained('jobset') PathPart Args(0) {
$c->stash->{jobset}->builds->delete_all;
$c->stash->{jobset}->delete;
});
return $c->res->redirect($c->uri_for($c->controller('Project')->action_for("view"), [$c->stash->{project}->name]));
return $c->res->redirect($c->uri_for($c->controller('Project')->action_for("project"), [$c->stash->{project}->name]));
}
txn_do($c->model('DB')->schema, sub {
updateJobset($c, $c->stash->{jobset});
});
$c->res->redirect($c->uri_for($self->action_for("index"),
[$c->stash->{project}->name, $c->stash->{jobset}->name]) . "#tabs-configuration");
my $newName = trim $c->stash->{params}->{name};
my $oldName = trim $c->stash->{jobset}->name;
unless ($oldName eq $newName) {
$c->stash->{params}->{oldName} = $oldName;
$c->stash->{jobsetName} = $newName;
undef $c->stash->{jobset};
}
jobset_PUT($self, $c);
}
@ -153,32 +262,16 @@ sub nixExprPathFromParams {
my ($c) = @_;
# The Nix expression path must be relative and can't contain ".." elements.
my $nixExprPath = trim $c->request->params->{"nixexprpath"};
my $nixExprPath = trim $c->stash->{params}->{"nixexprpath"};
error($c, "Invalid Nix expression path: $nixExprPath") if $nixExprPath !~ /^$relPathRE$/;
my $nixExprInput = trim $c->request->params->{"nixexprinput"};
my $nixExprInput = trim $c->stash->{params}->{"nixexprinput"};
error($c, "Invalid Nix expression input name: $nixExprInput") unless $nixExprInput =~ /^\w+$/;
return ($nixExprPath, $nixExprInput);
}
sub checkInput {
my ($c, $baseName) = @_;
my $inputName = trim $c->request->params->{"input-$baseName-name"};
error($c, "Invalid input name: $inputName") unless $inputName =~ /^[[:alpha:]]\w*$/;
my $inputType = trim $c->request->params->{"input-$baseName-type"};
error($c, "Invalid input type: $inputType") unless
$inputType eq "svn" || $inputType eq "svn-checkout" || $inputType eq "hg" || $inputType eq "tarball" ||
$inputType eq "string" || $inputType eq "path" || $inputType eq "boolean" || $inputType eq "bzr" || $inputType eq "bzr-checkout" ||
$inputType eq "git" || $inputType eq "build" || $inputType eq "sysbuild" ;
return ($inputName, $inputType);
}
sub checkInputValue {
my ($c, $type, $value) = @_;
$value = trim $value;
@ -191,50 +284,62 @@ sub checkInputValue {
sub updateJobset {
my ($c, $jobset) = @_;
my $jobsetName = trim $c->request->params->{"name"};
my $jobsetName = $c->stash->{jobsetName} or $jobset->name;
error($c, "Invalid jobset name: $jobsetName") if $jobsetName !~ /^$jobsetNameRE$/;
# When the expression is in a .scm file, assume it's a Guile + Guix
# build expression.
my $exprType =
$c->request->params->{"nixexprpath"} =~ /.scm$/ ? "guile" : "nix";
$c->stash->{params}->{"nixexprpath"} =~ /.scm$/ ? "guile" : "nix";
my ($nixExprPath, $nixExprInput) = nixExprPathFromParams $c;
$jobset->update(
{ name => $jobsetName
, description => trim($c->request->params->{"description"})
, description => trim($c->stash->{params}->{"description"})
, nixexprpath => $nixExprPath
, nixexprinput => $nixExprInput
, enabled => defined $c->request->params->{enabled} ? 1 : 0
, enableemail => defined $c->request->params->{enableemail} ? 1 : 0
, emailoverride => trim($c->request->params->{emailoverride}) || ""
, hidden => defined $c->request->params->{visible} ? 0 : 1
, keepnr => int(trim($c->request->params->{keepnr})) || 3
, checkinterval => int(trim($c->request->params->{checkinterval}))
, enabled => defined $c->stash->{params}->{enabled} ? 1 : 0
, enableemail => defined $c->stash->{params}->{enableemail} ? 1 : 0
, emailoverride => trim($c->stash->{params}->{emailoverride}) || ""
, hidden => defined $c->stash->{params}->{visible} ? 0 : 1
, keepnr => int(trim($c->stash->{params}->{keepnr})) || 3
, checkinterval => int(trim($c->stash->{params}->{checkinterval}))
, triggertime => $jobset->triggertime // time()
});
my %inputNames;
# Process the inputs of this jobset.
foreach my $param (keys %{$c->request->params}) {
unless (defined $c->stash->{params}->{inputs}) {
$c->stash->{params}->{inputs} = {};
foreach my $param (keys %{$c->stash->{params}}) {
next unless $param =~ /^input-(\w+)-name$/;
my $baseName = $1;
next if $baseName eq "template";
$c->stash->{params}->{inputs}->{$c->stash->{params}->{$param}} = { type => $c->stash->{params}->{"input-$baseName-type"}, values => $c->stash->{params}->{"input-$baseName-values"} };
unless ($baseName =~ /^\d+$/) { # non-numeric base name is an existing entry
$c->stash->{params}->{inputs}->{$c->stash->{params}->{$param}}->{oldName} = $baseName;
}
}
}
my ($inputName, $inputType) = checkInput($c, $baseName);
foreach my $inputName (keys %{$c->stash->{params}->{inputs}}) {
my $inputData = $c->stash->{params}->{inputs}->{$inputName};
error($c, "Invalid input name: $inputName") unless $inputName =~ /^[[:alpha:]]\w*$/;
$inputNames{$inputName} = 1;
my $inputType = $inputData->{type};
error($c, "Invalid input type: $inputType") unless
$inputType eq "svn" || $inputType eq "svn-checkout" || $inputType eq "hg" || $inputType eq "tarball" ||
$inputType eq "string" || $inputType eq "path" || $inputType eq "boolean" || $inputType eq "bzr" || $inputType eq "bzr-checkout" ||
$inputType eq "git" || $inputType eq "build" || $inputType eq "sysbuild" ;
my $input;
if ($baseName =~ /^\d+$/) { # numeric base name is auto-generated, i.e. a new entry
$input = $jobset->jobsetinputs->create(
unless (defined $inputData->{oldName}) {
$input = $jobset->jobsetinputs->update_or_create(
{ name => $inputName
, type => $inputType
});
} else { # it's an existing input
$input = ($jobset->jobsetinputs->search({name => $baseName}))[0];
$input = ($jobset->jobsetinputs->search({name => $inputData->{oldName}}))[0];
die unless defined $input;
$input->update({name => $inputName, type => $inputType});
}
@ -242,7 +347,7 @@ sub updateJobset {
# 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->{"input-$baseName-values"};
my $values = $inputData->{values};
$values = [] unless defined $values;
$values = [$values] unless ref($values) eq 'ARRAY';
my $altnr = 0;
@ -255,12 +360,12 @@ sub updateJobset {
# Get rid of deleted inputs.
my @inputs = $jobset->jobsetinputs->all;
foreach my $input (@inputs) {
$input->delete unless defined $inputNames{$input->name};
$input->delete unless defined $c->stash->{params}->{inputs}->{$input->name};
}
}
sub clone : Chained('jobset') PathPart('clone') Args(0) {
sub clone : Chained('jobsetChain') PathPart('clone') Args(0) {
my ($self, $c) = @_;
my $jobset = $c->stash->{jobset};
@ -270,14 +375,14 @@ sub clone : Chained('jobset') PathPart('clone') Args(0) {
}
sub clone_submit : Chained('jobset') PathPart('clone/submit') Args(0) {
sub clone_submit : Chained('jobsetChain') PathPart('clone/submit') Args(0) {
my ($self, $c) = @_;
my $jobset = $c->stash->{jobset};
requireProjectOwner($c, $jobset->project);
requirePost($c);
my $newJobsetName = trim $c->request->params->{"newjobset"};
my $newJobsetName = trim $c->stash->{params}->{"newjobset"};
error($c, "Invalid jobset name: $newJobsetName") unless $newJobsetName =~ /^[[:alpha:]][\w\-]*$/;
my $newJobset;
@ -304,7 +409,9 @@ sub clone_submit : Chained('jobset') PathPart('clone/submit') Args(0) {
}
sub evals : Chained('jobset') PathPart('evals') Args(0) {
sub evals :Chained('jobsetChain') :PathPart('evals') :Args(0) :ActionClass('REST') { }
sub evals_GET {
my ($self, $c) = @_;
$c->stash->{template} = 'evals.tt';
@ -318,12 +425,45 @@ sub evals : Chained('jobset') PathPart('evals') Args(0) {
$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)
my $offset = ($page - 1) * $resultsPerPage;
$c->stash->{evals} = getEvals($self, $c, $evals, $offset, $resultsPerPage);
my %entity = (
evals => [ $evals->search({ 'me.hasnewbuilds' => 1 }, {
columns => [
'me.hasnewbuilds',
'me.id',
'jobsetevalinputs.name',
'jobsetevalinputs.altnr',
'jobsetevalinputs.revision',
'jobsetevalinputs.type',
'jobsetevalinputs.uri',
'jobsetevalinputs.dependency',
'jobsetevalmembers.build',
],
join => [ 'jobsetevalinputs', 'jobsetevalmembers' ],
collapse => 1,
rows => $resultsPerPage,
offset => $offset,
order_by => "me.id DESC",
}) ],
first => "?page=1",
last => "?page=" . POSIX::ceil($c->stash->{total}/$resultsPerPage)
);
if ($page > 1) {
$entity{previous} = "?page=" . ($page - 1);
}
if ($page < POSIX::ceil($c->stash->{total}/$resultsPerPage)) {
$entity{next} = "?page=" . ($page + 1);
}
$self->status_ok(
$c,
entity => \%entity
);
}
# Redirect to the latest finished evaluation of this jobset.
sub latest_eval : Chained('jobset') PathPart('latest-eval') {
sub latest_eval : Chained('jobsetChain') PathPart('latest-eval') {
my ($self, $c, @args) = @_;
my $eval = getLatestFinishedEval($c, $c->stash->{jobset})
or notFound($c, "No evaluation found.");

View file

@ -7,17 +7,43 @@ use Hydra::Helper::Nix;
use Hydra::Helper::CatalystUtils;
sub project : Chained('/') PathPart('project') CaptureArgs(1) {
sub projectChain :Chained('/') :PathPart('project') :CaptureArgs(1) {
my ($self, $c, $projectName) = @_;
my $project = $c->model('DB::Projects')->find($projectName)
or notFound($c, "Project $projectName doesn't exist.");
my $project = $c->model('DB::Projects')->find($projectName, { columns => [
"me.name",
"me.displayName",
"me.description",
"me.enabled",
"me.hidden",
"me.homepage",
"owner.username",
"owner.fullname",
"views.name",
"releases.name",
"releases.timestamp",
"jobsets.name",
], join => [ 'owner', 'views', 'releases', 'jobsets' ], order_by => { -desc => "releases.timestamp" }, collapse => 1 });
if ($project) {
$c->stash->{project} = $project;
} else {
if ($c->action->name eq "project" and $c->request->method eq "PUT") {
$c->stash->{projectName} = $projectName;
} else {
$self->status_not_found(
$c,
message => "Project $projectName doesn't exist."
);
$c->detach;
}
}
}
sub view : Chained('project') PathPart('') Args(0) {
sub project :Chained('projectChain') :PathPart('') :Args(0) :ActionClass('REST::ForBrowsers') { }
sub project_GET {
my ($self, $c) = @_;
$c->stash->{template} = 'project.tt';
@ -26,10 +52,83 @@ sub view : Chained('project') PathPart('') Args(0) {
$c->stash->{jobsets} = [jobsetOverview($c, $c->stash->{project})];
$c->stash->{releases} = [$c->stash->{project}->releases->search({},
{order_by => ["timestamp DESC"]})];
$self->status_ok(
$c,
entity => $c->stash->{project}
);
}
sub project_PUT {
my ($self, $c) = @_;
if (defined $c->stash->{project}) {
error($c, "Cannot rename project `$c->stash->{params}->{oldName}' over existing project `$c->stash->{project}->name") if defined $c->stash->{params}->{oldName};
requireProjectOwner($c, $c->stash->{project});
txn_do($c->model('DB')->schema, sub {
updateProject($c, $c->stash->{project});
});
if ($c->req->looks_like_browser) {
$c->res->redirect($c->uri_for($self->action_for("project"), [$c->stash->{project}->name]) . "#tabs-configuration");
} else {
$self->status_no_content($c);
}
} elsif (defined $c->stash->{params}->{oldName}) {
my $project = $c->model('DB::Projects')->find($c->stash->{params}->{oldName});
if (defined $project) {
requireProjectOwner($c, $project);
txn_do($c->model('DB')->schema, sub {
updateProject($c, $project);
});
my $uri = $c->uri_for($self->action_for("project"), [$project->name]);
if ($c->req->looks_like_browser) {
$c->res->redirect($uri . "#tabs-configuration");
} else {
$self->status_created(
$c,
location => "$uri",
entity => { name => $project->name, uri => "$uri", type => "project" }
);
}
} else {
$self->status_not_found(
$c,
message => "Project $c->stash->{params}->{oldName} doesn't exist."
);
}
} else {
requireMayCreateProjects($c);
error($c, "Invalid project name: $c->stash->{projectName}") if $c->stash->{projectName} !~ /^$projectNameRE$/;
my $project;
txn_do($c->model('DB')->schema, sub {
# Note: $projectName is validated in updateProject,
# which will abort the transaction if the name isn't
# valid. Idem for the owner.
my $owner = $c->user->username;
$project = $c->model('DB::Projects')->create(
{name => $c->stash->{projectName}, displayname => "", owner => $owner});
updateProject($c, $project);
});
my $uri = $c->uri_for($self->action_for("project"), [$project->name]);
if ($c->req->looks_like_browser) {
$c->res->redirect($uri . "#tabs-configuration");
} else {
$self->status_created(
$c,
location => "$uri",
entity => { name => $project->name, uri => "$uri", type => "project" }
);
}
}
}
sub edit : Chained('project') PathPart Args(0) {
sub edit : Chained('projectChain') PathPart Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
@ -39,12 +138,10 @@ sub edit : Chained('project') PathPart Args(0) {
}
sub submit : Chained('project') PathPart Args(0) {
sub submit : Chained('projectChain') PathPart Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
requirePost($c);
if (($c->request->params->{submit} // "") eq "delete") {
txn_do($c->model('DB')->schema, sub {
$c->stash->{project}->jobsetevals->delete_all;
@ -54,11 +151,14 @@ sub submit : Chained('project') PathPart Args(0) {
return $c->res->redirect($c->uri_for("/"));
}
txn_do($c->model('DB')->schema, sub {
updateProject($c, $c->stash->{project});
});
$c->res->redirect($c->uri_for($self->action_for("view"), [$c->stash->{project}->name]) . "#tabs-configuration");
my $newName = trim $c->stash->{params}->{name};
my $oldName = trim $c->stash->{project}->name;
unless ($oldName eq $newName) {
$c->stash->{params}->{oldName} = $oldName;
$c->stash->{projectName} = $newName;
undef $c->stash->{project};
}
project_PUT($self, $c);
}
@ -86,28 +186,13 @@ sub create : Path('/create-project') {
sub create_submit : Path('/create-project/submit') {
my ($self, $c) = @_;
requireMayCreateProjects($c);
$c->stash->{projectName} = trim $c->stash->{params}->{name};
my $projectName = trim $c->request->params->{name};
error($c, "Invalid project name: $projectName") if $projectName !~ /^$projectNameRE$/;
txn_do($c->model('DB')->schema, sub {
# Note: $projectName is validated in updateProject,
# which will abort the transaction if the name isn't
# valid. Idem for the owner.
my $owner = $c->check_user_roles('admin')
? trim $c->request->params->{owner} : $c->user->username;
my $project = $c->model('DB::Projects')->create(
{name => $projectName, displayname => "", owner => $owner});
updateProject($c, $project);
});
$c->res->redirect($c->uri_for($self->action_for("view"), [$projectName]));
project_PUT($self, $c);
}
sub create_jobset : Chained('project') PathPart('create-jobset') Args(0) {
sub create_jobset : Chained('projectChain') PathPart('create-jobset') Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
@ -118,27 +203,12 @@ sub create_jobset : Chained('project') PathPart('create-jobset') Args(0) {
}
sub create_jobset_submit : Chained('project') PathPart('create-jobset/submit') Args(0) {
sub create_jobset_submit : Chained('projectChain') PathPart('create-jobset/submit') Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
$c->stash->{jobsetName} = trim $c->stash->{params}->{name};
my $jobsetName = trim $c->request->params->{name};
my $exprType =
$c->request->params->{"nixexprpath"} =~ /.scm$/ ? "guile" : "nix";
error($c, "Invalid jobset name: $jobsetName") if $jobsetName !~ /^$jobsetNameRE$/;
txn_do($c->model('DB')->schema, sub {
# Note: $jobsetName is validated in updateProject, which will
# abort the transaction if the name isn't valid.
my $jobset = $c->stash->{project}->jobsets->create(
{name => $jobsetName, nixexprinput => "", nixexprpath => "", emailoverride => ""});
Hydra::Controller::Jobset::updateJobset($c, $jobset);
});
$c->res->redirect($c->uri_for($c->controller('Jobset')->action_for("index"),
[$c->stash->{project}->name, $jobsetName]));
Hydra::Controller::Jobset::jobset_PUT($self, $c);
}
@ -146,32 +216,32 @@ sub updateProject {
my ($c, $project) = @_;
my $owner = $project->owner;
if ($c->check_user_roles('admin')) {
$owner = trim $c->request->params->{owner};
if ($c->check_user_roles('admin') and defined $c->stash->{params}->{owner}) {
$owner = trim $c->stash->{params}->{owner};
error($c, "Invalid owner: $owner")
unless defined $c->model('DB::Users')->find({username => $owner});
}
my $projectName = trim $c->request->params->{name};
my $projectName = $c->stash->{projectName} or $project->name;
error($c, "Invalid project name: $projectName") if $projectName !~ /^$projectNameRE$/;
my $displayName = trim $c->request->params->{displayname};
my $displayName = trim $c->stash->{params}->{displayname};
error($c, "Invalid display name: $displayName") if $displayName eq "";
$project->update(
{ name => $projectName
, displayname => $displayName
, description => trim($c->request->params->{description})
, homepage => trim($c->request->params->{homepage})
, enabled => defined $c->request->params->{enabled} ? 1 : 0
, hidden => defined $c->request->params->{visible} ? 0 : 1
, description => trim($c->stash->{params}->{description})
, homepage => trim($c->stash->{params}->{homepage})
, enabled => defined $c->stash->{params}->{enabled} ? 1 : 0
, hidden => defined $c->stash->{params}->{visible} ? 0 : 1
, owner => $owner
});
}
# Hydra::Base::Controller::ListBuilds needs this.
sub get_builds : Chained('project') PathPart('') CaptureArgs(0) {
sub get_builds : Chained('projectChain') PathPart('') CaptureArgs(0) {
my ($self, $c) = @_;
$c->stash->{allBuilds} = $c->stash->{project}->builds;
$c->stash->{jobStatus} = $c->model('DB')->resultset('JobStatusForProject')
@ -184,7 +254,7 @@ sub get_builds : Chained('project') PathPart('') CaptureArgs(0) {
}
sub create_view_submit : Chained('project') PathPart('create-view/submit') Args(0) {
sub create_view_submit : Chained('projectChain') PathPart('create-view/submit') Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
@ -204,7 +274,7 @@ sub create_view_submit : Chained('project') PathPart('create-view/submit') Args(
}
sub create_view : Chained('project') PathPart('create-view') Args(0) {
sub create_view : Chained('projectChain') PathPart('create-view') Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
@ -214,7 +284,7 @@ sub create_view : Chained('project') PathPart('create-view') Args(0) {
}
sub create_release : Chained('project') PathPart('create-release') Args(0) {
sub create_release : Chained('projectChain') PathPart('create-release') Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
$c->stash->{template} = 'edit-release.tt';
@ -222,7 +292,7 @@ sub create_release : Chained('project') PathPart('create-release') Args(0) {
}
sub create_release_submit : Chained('project') PathPart('create-release/submit') Args(0) {
sub create_release_submit : Chained('projectChain') PathPart('create-release/submit') Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});

View file

@ -66,13 +66,13 @@ sub submit : Chained('release') PathPart('submit') Args(0) {
txn_do($c->model('DB')->schema, sub {
$c->stash->{release}->delete;
});
$c->res->redirect($c->uri_for($c->controller('Project')->action_for('view'),
$c->res->redirect($c->uri_for($c->controller('Project')->action_for('project'),
[$c->stash->{project}->name]));
} else {
txn_do($c->model('DB')->schema, sub {
updateRelease($c, $c->stash->{release});
});
$c->res->redirect($c->uri_for($self->action_for("view"),
$c->res->redirect($c->uri_for($self->action_for("project"),
[$c->stash->{project}->name, $c->stash->{release}->name]));
}
}

View file

@ -37,23 +37,44 @@ sub begin :Private {
'sysbuild' => 'Build output (same system)'
};
$_->supportedInputTypes($c->stash->{inputTypes}) foreach @{$c->hydra_plugins};
$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;
}
}
sub deserialize :ActionClass('Deserialize') { }
sub index :Path :Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'overview.tt';
$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 })];
$self->status_ok(
$c,
entity => [$c->model('DB::Projects')->search(isAdmin($c) ? {} : {hidden => 0}, {
order_by => 'name',
columns => [ 'name', 'displayname' ]
})]
);
}
sub queue :Local {
sub queue :Local :Args(0) :ActionClass('REST') { }
sub queue_GET {
my ($self, $c) = @_;
$c->stash->{template} = 'queue.tt';
$c->stash->{queue} = [$c->model('DB::Builds')->search(
{finished => 0}, { join => ['project'], order_by => ["priority DESC", "id"], columns => [@buildListColumns], '+select' => ['project.enabled'], '+as' => ['enabled'] })];
$c->stash->{flashMsg} //= $c->flash->{buildMsg};
$self->status_ok(
$c,
entity => [$c->model('DB::Builds')->search(
{finished => 0}, { join => ['project'], order_by => ["priority DESC", "id"], columns => [@buildListColumns], '+select' => ['project.enabled'], '+as' => ['enabled'] })]
);
}
@ -71,13 +92,32 @@ sub timeline :Local {
}
sub status :Local {
sub status :Local :Args(0) :ActionClass('REST') { }
sub status_GET {
my ($self, $c) = @_;
$c->stash->{steps} = [ $c->model('DB::BuildSteps')->search(
$self->status_ok(
$c,
entity => [ $c->model('DB::BuildSteps')->search(
{ 'me.busy' => 1, 'build.finished' => 0, 'build.busy' => 1 },
{ join => [ 'build' ]
, order_by => [ 'machine' ]
} ) ];
{ join => { build => [ 'project', 'job', 'jobset' ] },
columns => [
'me.machine',
'me.system',
'me.stepnr',
'me.drvpath',
'me.starttime',
'build.id',
{
'build.project.name' => 'project.name',
'build.jobset.name' => 'jobset.name',
'build.job.name' => 'job.name'
}
],
order_by => [ 'machine' ]
}
) ]
);
}
@ -181,7 +221,8 @@ sub end : ActionClass('RenderView') {
$c->forward('View::JSON');
}
elsif (scalar @{$c->error}) {
if (scalar @{$c->error}) {
$c->stash->{resource} = { errors => "$c->error" };
$c->stash->{template} = 'error.tt';
$c->stash->{errors} = $c->error;
$c->response->status(500) if $c->response->status == 200;
@ -190,9 +231,19 @@ sub end : ActionClass('RenderView') {
$c->response->status . " " . HTTP::Status::status_message($c->response->status);
}
$c->clear_errors;
} elsif (defined $c->stash->{resource} and
(ref $c->stash->{resource} eq ref {}) and
defined $c->stash->{resource}->{error}) {
$c->stash->{template} = 'error.tt';
$c->stash->{httpStatus} =
$c->response->status . " " . HTTP::Status::status_message($c->response->status);
}
$c->forward('serialize');
}
sub serialize : ActionClass('Serialize') { }
sub nar :Local :Args(1) {
my ($self, $c, $path) = @_;

View file

@ -3,7 +3,7 @@ package Hydra::Controller::User;
use utf8;
use strict;
use warnings;
use base 'Catalyst::Controller';
use base 'Hydra::Base::Controller::REST';
use Crypt::RandPasswd;
use Digest::SHA1 qw(sha1_hex);
use Hydra::Helper::Nix;
@ -13,31 +13,60 @@ use Hydra::Helper::CatalystUtils;
__PACKAGE__->config->{namespace} = '';
sub login :Local {
sub login :Local :Args(0) :ActionClass('REST::ForBrowsers') { }
sub login_GET {
my ($self, $c) = @_;
my $username = $c->request->params->{username} || "";
my $password = $c->request->params->{password} || "";
if ($username eq "" && $password eq "" && !defined $c->session->{referer}) {
my $baseurl = $c->uri_for('/');
my $referer = $c->request->referer;
$c->session->{referer} = $referer if defined $referer && $referer =~ m/^($baseurl)/;
}
if ($username && $password) {
backToReferer($c) if $c->authenticate({username => $username, password => $password});
$c->stash->{errorMsg} = "Bad username or password.";
}
$c->stash->{template} = 'login.tt';
}
sub login_POST {
my ($self, $c) = @_;
sub logout :Local {
my $username;
my $password;
$username = $c->stash->{params}->{username};
$password = $c->stash->{params}->{password};
if ($username && $password) {
if ($c->authenticate({username => $username, password => $password})) {
if ($c->request->looks_like_browser) {
backToReferer($c);
} else {
currentUser_GET($self, $c);
}
} else {
$self->status_forbidden($c, message => "Bad username or password.");
if ($c->request->looks_like_browser) {
login_GET($self, $c);
}
}
}
}
sub logout :Local :Args(0) :ActionClass('REST::ForBrowsers') { }
sub logout_POST {
my ($self, $c) = @_;
$c->logout;
if ($c->request->looks_like_browser) {
$c->response->redirect($c->request->referer || $c->uri_for('/'));
} else {
$self->status_no_content($c);
}
}
sub logout_GET {
# Probably a better way to do this
my ($self, $c) = @_;
logout_POST($self, $c);
}
@ -116,6 +145,24 @@ sub register :Local Args(0) {
}
sub currentUser :Path('/current-user') :ActionClass('REST') { }
sub currentUser_GET {
my ($self, $c) = @_;
requireLogin($c) if !$c->user_exists;
$self->status_ok(
$c,
entity => $c->model('DB::Users')->find({ 'me.username' => $c->user->username}, {
columns => [ "me.fullname", "me.emailaddress", "me.username", "userroles.role" ]
, join => [ "userroles" ]
, collapse => 1
})
);
}
sub user :Chained('/') PathPart('user') CaptureArgs(1) {
my ($self, $c, $userName) = @_;
@ -139,7 +186,9 @@ sub deleteUser {
}
sub edit :Chained('user') Args(0) {
sub edit :Chained('user') :Args(0) :ActionClass('REST::ForBrowsers') { }
sub edit_GET {
my ($self, $c) = @_;
my $user = $c->stash->{user};
@ -148,18 +197,26 @@ sub edit :Chained('user') Args(0) {
$c->session->{referer} = $c->request->referer if !defined $c->session->{referer};
if ($c->request->method ne "POST") {
$c->stash->{fullname} = $user->fullname;
$c->stash->{emailonerror} = $user->emailonerror;
return;
}
if (($c->request->params->{submit} // "") eq "delete") {
$c->stash->{emailonerror} = $user->emailonerror;
}
sub edit_POST {
my ($self, $c) = @_;
my $user = $c->stash->{user};
$c->stash->{template} = 'user.tt';
$c->session->{referer} = $c->request->referer if !defined $c->session->{referer};
if (($c->stash->{params}->{submit} // "") eq "delete") {
deleteUser($self, $c, $user);
backToReferer($c);
}
if (($c->request->params->{submit} // "") eq "reset-password") {
if (($c->stash->{params}->{submit} // "") eq "reset-password") {
$c->stash->{json} = {};
error($c, "No email address is set for this user.")
unless $user->emailaddress;
@ -176,7 +233,7 @@ sub edit :Chained('user') Args(0) {
return;
}
my $fullName = trim $c->req->params->{fullname};
my $fullName = trim $c->stash->{params}->{fullname};
txn_do($c->model('DB')->schema, sub {
@ -184,15 +241,15 @@ sub edit :Chained('user') Args(0) {
$user->update(
{ fullname => $fullName
, emailonerror => $c->request->params->{"emailonerror"} ? 1 : 0
, emailonerror => $c->stash->{params}->{"emailonerror"} ? 1 : 0
});
my $password = $c->req->params->{password} // "";
my $password = $c->stash->{params}->{password} // "";
if ($password ne "") {
error($c, "You must specify a password of at least 6 characters.")
unless isValidPassword($password);
error($c, "The passwords you specified did not match.")
if $password ne trim $c->req->params->{password2};
if $password ne trim $c->stash->{params}->{password2};
setPassword($user, $password);
}
@ -204,7 +261,11 @@ sub edit :Chained('user') Args(0) {
});
if ($c->request->looks_like_browser) {
backToReferer($c);
} else {
$self->status_no_content($c);
}
}

View file

@ -118,7 +118,7 @@ sub submit : Chained('view') PathPart('submit') Args(0) {
requireProjectOwner($c, $c->stash->{project});
if (($c->request->params->{submit} || "") eq "delete") {
$c->stash->{view}->delete;
$c->res->redirect($c->uri_for($c->controller('Project')->action_for('view'),
$c->res->redirect($c->uri_for($c->controller('Project')->action_for('project'),
[$c->stash->{project}->name]));
}
txn_do($c->model('DB')->schema, sub {
@ -224,7 +224,7 @@ sub result : Chained('view') PathPart('') {
notFound($c, "View doesn't have a job named $jobName" . ($system ? " for $system" : "") . ".")
unless defined $build;
error($c, "Job `$jobName' isn't unique.") if @others;
return $c->res->redirect($c->uri_for($c->controller('Build')->action_for('view_build'),
return $c->res->redirect($c->uri_for($c->controller('Build')->action_for('build'),
[$build->{build}->id], @args));
}
}

View file

@ -202,7 +202,7 @@ sub sendEmail {
# always returns a request parameter as a list.
sub paramToList {
my ($c, $name) = @_;
my $x = $c->request->params->{$name};
my $x = $c->stash->{params}->{$name};
return () unless defined $x;
return @$x if ref($x) eq 'ARRAY';
return ($x);

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<BuildInputs>
=cut
@ -156,7 +168,7 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:byU/SLN03zNJlSFbi/3Bcg
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:tKZAybbNaRIMs9n5tHkqPw
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<BuildOutputs>
=cut
@ -82,8 +94,8 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-30 16:22:11
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:UpVoKdd3OwMvlvyMjcYNVA
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:V8MbzKvZNEaeHBJV67+ZMQ
# You can replace this text with custom code or comments, and it will be preserved on regeneration

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<BuildProducts>
=cut
@ -138,8 +150,8 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:KHwh/Np40jxKXc3ijMImEQ
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:+0LkZiaRL5tGJvbLxnwD/g
# You can replace this text with custom content, and it will be preserved on regeneration
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<BuildStepOutputs>
=cut
@ -107,8 +119,8 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-30 16:22:11
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dC1yX7arRVu9K3wG9dAjCg
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:A/4v3ugXYbuYoKPlOvC6mg
# You can replace this text with custom code or comments, and it will be preserved on regeneration

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<BuildSteps>
=cut
@ -154,7 +166,7 @@ __PACKAGE__->has_many(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-30 16:36:03
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ZiA1nv73Fpp0/DTi4sLfEQ
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:OZsXJniZ/7EB2iSz7p5y4A
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<Builds>
=cut
@ -457,8 +469,8 @@ __PACKAGE__->has_many(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-05-03 14:35:11
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:aYVEk+AeDsgTRi5GAqOhEw
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:isCEXACY/PwkvgKHcXvAIg
__PACKAGE__->has_many(
"dependents",

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<CachedBazaarInputs>
=cut
@ -71,8 +83,8 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key("uri", "revision");
# Created by DBIx::Class::Schema::Loader v0.07014 @ 2011-12-05 14:15:43
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ONhBo6Xhq7uwYFdEzbp3dg
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:zvun8uhxwrr7B8EsqBoCjA
# You can replace this text with custom content, and it will be preserved on regeneration

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<CachedCVSInputs>
=cut
@ -87,8 +99,8 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key("uri", "module", "sha256hash");
# Created by DBIx::Class::Schema::Loader v0.07014 @ 2011-12-05 14:15:43
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:IcSVN/tlfQQtX88Ix+aKnw
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Vi1qzjW52Lnsl0JSmGzy0w
# You can replace this text with custom content, and it will be preserved on regeneration
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<CachedGitInputs>
=cut
@ -80,7 +92,7 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key("uri", "branch", "revision");
# Created by DBIx::Class::Schema::Loader v0.07014 @ 2011-12-05 14:15:43
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:fx3yosWMmJ+MnvL/dSWtFA
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:I4hI02FKRMkw76WV/KBocA
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<CachedHgInputs>
=cut
@ -80,8 +92,8 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key("uri", "branch", "revision");
# Created by DBIx::Class::Schema::Loader v0.07014 @ 2011-12-05 14:15:43
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:xFLnuCBAcJCg+N3b4aajZQ
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qS/eiiZXmpc7KpTHdtaT7g
# You can replace this text with custom content, and it will be preserved on regeneration

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<CachedPathInputs>
=cut
@ -78,7 +90,7 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key("srcpath", "sha256hash");
# Created by DBIx::Class::Schema::Loader v0.07014 @ 2011-12-05 14:15:43
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:4KzXhMnUldVgNuuNXWIYjw
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:28rja0vR1glJJ15hzVfjsQ
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<CachedSubversionInputs>
=cut
@ -71,7 +83,7 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key("uri", "revision");
# Created by DBIx::Class::Schema::Loader v0.07014 @ 2011-12-05 14:15:43
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:1rjwWtZXGEowHqhfjLqjmA
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:3qXfnvkOVj25W94bfhQ65w
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<Jobs>
=cut
@ -126,7 +138,7 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-05-23 16:09:46
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:JgxEaCz/TW9YKa+HavRzXw
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:t2CCfUjFEz/lO4szROz1AQ
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<JobsetEvalInputs>
=cut
@ -154,8 +166,8 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ng+Q6tMX5EJMD7DxRWVy7Q
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:1Dp8B58leBLh4GK0GPw2zg
# You can replace this text with custom code or comments, and it will be preserved on regeneration

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<JobsetEvalMembers>
=cut
@ -98,8 +110,8 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:EVwSR9WBqbBdIHq1ANQMHg
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ccPNQe/QnSjTAC3uGWe8Ng
# You can replace this text with custom content, and it will be preserved on regeneration

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<JobsetEvals>
=cut
@ -176,8 +188,8 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qElGj6zzuI0xo426np3r1w
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:SlEiF8oN6FBK262uSiMKiw
__PACKAGE__->has_many(
"buildIds",

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<JobsetInputAlts>
=cut
@ -109,7 +121,7 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:M3pNBRLfxgSScrPj1zaajA
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:UUO37lIuEYm0GiR92m/fyA
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<JobsetInputs>
=cut
@ -130,7 +142,7 @@ __PACKAGE__->has_many(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:xjioYUPo6visoLAVDkDZ0Q
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:UXBzqO0vHPql4LYyXpgEQg
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<Jobsets>
=cut
@ -260,7 +272,7 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-05-02 14:50:55
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:q4amPCWRoWMThnRa/n/y1w
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:tsGR8MhZRIUeNwpcVczMUw
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<NewsItems>
=cut
@ -88,7 +100,7 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:lnA5Utkwk5WTyKA/M5mlyg
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:3CRNsvd+YnZp9c80tuZREQ
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<ProjectMembers>
=cut
@ -91,8 +103,8 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:zW87n6E7xWaShcFbgFkVuw
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:imPoiaitrTbX0vVNlF6dPA
# You can replace this text with custom content, and it will be preserved on regeneration

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<Projects>
=cut
@ -255,8 +267,8 @@ Composing rels: L</projectmembers> -> username
__PACKAGE__->many_to_many("usernames", "projectmembers", "username");
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:OCuhmxs8pZxvmk81eVLLcQ
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:RffghAo9jAaqYk41y1Sdqw
# These lines were loaded from '/home/rbvermaa/src/hydra/src/lib/Hydra/Schema/Projects.pm' found in @INC.
# They are now part of the custom portion of this file
# for you to hand-edit. If you do not either delete

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<ReleaseMembers>
=cut
@ -123,7 +135,7 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:eP00w5UJp1uTtiB7D5IhTQ
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:7M7WPlGQT6rNHKJ+82/KSA
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<Releases>
=cut
@ -107,7 +119,7 @@ __PACKAGE__->has_many(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:UTUE3Hb89fT7prwnwwBgvQ
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qISBiwvboB8dIdinaE45mg
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<SchemaVersion>
=cut
@ -33,8 +45,8 @@ __PACKAGE__->table("SchemaVersion");
__PACKAGE__->add_columns("version", { data_type => "integer", is_nullable => 0 });
# Created by DBIx::Class::Schema::Loader v0.07014 @ 2012-02-29 00:47:18
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:LFD28W0GvvrOOylCM98SEQ
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:08/7gbEQp1TqBiWFJXVY0w
# You can replace this text with custom code or comments, and it will be preserved on regeneration

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<SystemTypes>
=cut
@ -56,7 +68,7 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key("system");
# Created by DBIx::Class::Schema::Loader v0.07014 @ 2011-12-05 14:15:43
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:zg8db3Cbi0QOv+gLJqH8cQ
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8cC34cEw9T3+x+7uRs4KHQ
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<UriRevMapper>
=cut
@ -55,8 +67,8 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key("baseuri");
# Created by DBIx::Class::Schema::Loader v0.07014 @ 2011-12-05 14:15:43
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hzKzGAgAiCfU0nBOiDnjWw
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:G2GAF/Rb7cRkRegH94LwIA
# You can replace this text with custom content, and it will be preserved on regeneration

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<UserRoles>
=cut
@ -75,7 +87,7 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:KArPHyemtnm/siwE4x5mGQ
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:aS+ivlFpndqIv8U578zz9A
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<Users>
=cut
@ -149,8 +161,8 @@ Composing rels: L</projectmembers> -> project
__PACKAGE__->many_to_many("projects", "projectmembers", "project");
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:OAUFl/teGpfeleb6D8FPlw
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hy3MKvFxfL+1bTc7Hcb1zA
# These lines were loaded from '/home/rbvermaa/src/hydra/src/lib/Hydra/Schema/Users.pm' found in @INC.
# They are now part of the custom portion of this file
# for you to hand-edit. If you do not either delete

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<ViewJobs>
=cut
@ -139,7 +151,7 @@ __PACKAGE__->belongs_to(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:cbSUw113ENPypbd/sICfgg
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hz912vBfYw0rHslBPqJW2w
1;

View file

@ -15,6 +15,18 @@ use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<Views>
=cut
@ -105,7 +117,7 @@ __PACKAGE__->has_many(
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-01-22 13:29:36
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Vyd2+0RAF3XGTpq3KswfAQ
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:U23GZ3k5KZk2go6j2LYLHA
1;

View file

@ -9,6 +9,7 @@ PERL_MODULES = \
$(wildcard Hydra/Base/*.pm) \
$(wildcard Hydra/Base/Controller/*.pm) \
$(wildcard Hydra/Script/*.pm) \
$(wildcard Hydra/Component/*.pm) \
$(wildcard Hydra/Plugin/*.pm)
EXTRA_DIST = \

View file

@ -7,7 +7,7 @@
[% ELSE %]
[% INCLUDE renderBuildList builds=queue showSchedulingInfo=1 hideResultInfo=1 %]
[% INCLUDE renderBuildList builds=resource showSchedulingInfo=1 hideResultInfo=1 %]
[% END %]

View file

@ -6,7 +6,7 @@
<tr><th>Machine</th><th>Job</th><th>Type</th><th>Build</th><th>Step</th><th>What</th><th>Since</th></tr>
</thead>
<tbody>
[% FOREACH step IN steps %]
[% FOREACH step IN resource %]
<tr>
<td><tt>[% IF step.machine; step.machine.match('@(.*)').0; ELSE; 'localhost'; END %]</tt></td>
<td><tt>[% INCLUDE renderFullJobName project = step.build.project.name jobset = step.build.jobset.name job = step.build.job.name %]</tt></td>

View file

@ -37,7 +37,7 @@
[% WRAPPER makeSubMenu title="Project" %]
<li class="nav-header">[% HTML.escape(project.name) %]</li>
<li class="divider"></li>
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('view'), [project.name]) title = "Overview" %]
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('project'), [project.name]) title = "Overview" %]
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('all'), [project.name]) title = "Latest builds" %]
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('jobstatus'), [project.name]) title = "Job status" %]
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('errors'), [project.name]) title = "Errors" %]
@ -56,7 +56,7 @@
<li class="nav-header">[% HTML.escape(jobset.name) %]</li>
<li class="divider"></li>
[% INCLUDE menuItem
uri = c.uri_for(c.controller('Jobset').action_for('index'), [project.name, jobset.name])
uri = c.uri_for(c.controller('Jobset').action_for('jobset'), [project.name, jobset.name])
title = "Overview" %]
[% INCLUDE menuItem
uri = c.uri_for(c.controller('Jobset').action_for('evals'), [project.name, jobset.name])

View file

@ -16,4 +16,4 @@ hydra-sqlite.sql: hydra.sql
update-dbix: hydra-sqlite.sql
rm -f tmp.sqlite
sqlite3 tmp.sqlite < hydra-sqlite.sql
perl -MDBIx::Class::Schema::Loader=make_schema_at,dump_to_dir:../lib -e 'make_schema_at("Hydra::Schema", { naming => { ALL => "v5" }, relationships => 1, moniker_map => sub {return "$$_";} }, ["dbi:SQLite:tmp.sqlite"])'
perl -I ../lib -MDBIx::Class::Schema::Loader=make_schema_at,dump_to_dir:../lib -e 'make_schema_at("Hydra::Schema", { naming => { ALL => "v5" }, relationships => 1, moniker_map => sub {return "$$_";}, components => [ "+Hydra::Component::ToJSON" ], }, ["dbi:SQLite:tmp.sqlite"])'

12
tests/api-test.nix Normal file
View file

@ -0,0 +1,12 @@
let
builder = builtins.toFile "builder.sh" ''
echo -n ${builtins.readFile ./default.nix} > $out
'';
in {
job = derivation {
name = "job";
system = builtins.currentSystem;
builder = "/bin/sh";
args = [ builder ];
};
}

63
tests/api-test.pl Normal file
View file

@ -0,0 +1,63 @@
use LWP::UserAgent;
use JSON;
use Test::Simple tests => 15;
my $ua = LWP::UserAgent->new;
$ua->cookie_jar({});
sub request_json {
my ($opts) = @_;
my $req = HTTP::Request->new;
$req->method($opts->{method} or "GET");
$req->uri("http://localhost:3000$opts->{uri}");
$req->header(Accept => "application/json");
$req->content(encode_json($opts->{data})) if defined $opts->{data};
my $res = $ua->request($req);
print $res->as_string();
return $res;
}
my $result = request_json({ uri => "/login", method => "POST", data => { username => "root", password => "foobar" } });
my $user = decode_json($result->content());
ok($user->{username} eq "root", "The root user is named root");
ok($user->{userroles}->[0]->{role} eq "admin", "The root user is an admin");
$user = decode_json(request_json({ uri => "/current-user" })->content());
ok($user->{username} eq "root", "The current user is named root");
ok($user->{userroles}->[0]->{role} eq "admin", "The current user is an admin");
ok(request_json({ uri => '/project/sample' })->code() == 404, "Non-existent projects don't exist");
$result = request_json({ uri => '/project/sample', method => 'PUT', data => { displayname => "Sample", enabled => "1", } });
ok($result->code() == 201, "PUTting a new project creates it");
my $project = decode_json(request_json({ uri => '/project/sample' })->content());
ok((not @{$project->{jobsets}}), "A new project has no jobsets");
$result = request_json({ uri => '/jobset/sample/default', method => 'PUT', data => { nixexprpath => "default.nix", nixexprinput => "src", inputs => { src => { type => "path", values => "/run/jobset" } }, enabled => "1", checkinterval => "3600"} });
ok($result->code() == 201, "PUTting a new jobset creates it");
my $jobset = decode_json(request_json({ uri => '/jobset/sample/default' })->content());
ok($jobset->{jobsetinputs}->[0]->{name} eq "src", "The new jobset has an 'src' input");
ok($jobset->{jobsetinputs}->[0]->{jobsetinputalts}->[0]->{value} eq "/run/jobset", "The 'src' input is in /run/jobset");
system("LOGNAME=root NIX_STORE_DIR=/run/nix/store NIX_LOG_DIR=/run/nix/var/log/nix NIX_STATE_DIR=/run/nix/var/nix HYDRA_DATA=/var/lib/hydra HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' hydra-evaluator sample default");
$result = request_json({ uri => '/jobset/sample/default/evals' });
ok($result->code() == 200, "Can get evals of a jobset");
my $evals = decode_json($result->content())->{evals};
my $eval = $evals->[0];
ok($eval->{hasnewbuilds} == 1, "The first eval of a jobset has new builds");
# Ugh, cached for 30s
sleep 30;
system("echo >> /run/jobset/default.nix; LOGNAME=root NIX_STORE_DIR=/run/nix/store NIX_LOG_DIR=/run/nix/var/log/nix NIX_STATE_DIR=/run/nix/var/nix HYDRA_DATA=/var/lib/hydra HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' hydra-evaluator sample default");
my $evals = decode_json(request_json({ uri => '/jobset/sample/default/evals' })->content())->{evals};
ok($evals->[0]->{jobsetevalinputs}->[0]->{revision} != $evals->[1]->{jobsetevalinputs}->[0]->{revision}, "Changing a jobset source changes its revision");
my $build = decode_json(request_json({ uri => "/build/" . $evals->[0]->{jobsetevalmembers}->[0]->{build} })->content());
ok($build->{job} eq "job", "The build's job name is job");
ok($build->{finished} == 0, "The build isn't finished yet");