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:
@@ -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);
|
||||
|
||||
$c->stash->{project} = $project;
|
||||
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}) {
|
||||
next unless $param =~ /^input-(\w+)-name$/;
|
||||
my $baseName = $1;
|
||||
next if $baseName eq "template";
|
||||
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.");
|
||||
|
Reference in New Issue
Block a user