Skip to content

Commit

Permalink
rest api
Browse files Browse the repository at this point in the history
  • Loading branch information
vti committed May 2, 2017
1 parent 939125f commit 743f43f
Show file tree
Hide file tree
Showing 44 changed files with 1,078 additions and 672 deletions.
245 changes: 244 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Crafty is a dead simple but useful for personal projects CI server.
- [x] dynamic workers (inproc, fork or detach mode)
- [x] realtime updates
- [x] realtime log tails
- [ ] REST API
- [x] REST API
- [ ] webhook integration with GitHub, GitLab and BitBucket

## Configuration (YAML)
Expand Down Expand Up @@ -71,6 +71,249 @@ You have to have *Perl* :camel: and *SQLite3* installed.
$ bin/migrate
$ bin/crafty

## REST API

### Essentials

#### Client Errors

1. Invalid format

HTTP/1.1 400 Bad Request

{"error":"Invalid JSON"}

2. Validation errors

HTTP/1.1 422 Unprocessible Entity

{"error":"Invalid fields","fields":{"project":"Required"}}

#### Server Errors

1. Something bad happened

HTTP/1.1 500 System Error

{"error":"Oops"}

### Build Management

#### List Builds

GET /builds

**Response**

HTTP/1.1 200 Ok
Content-Type: application/json

{
"builds": [{
"status": "S",
"uuid": "d51ef218-2f1b-11e7-ab6d-4dcfdc676234",
"pid": 0,
"is_cancelable": "",
"created": "2017-05-02 11:43:44.430438+0200",
"finished": "2017-05-02 11:43:49.924477+0200",
"status_display": "success",
"is_new": "",
"branch": "master",
"project": "tu",
"is_restartable": "1",
"status_name": "Success",
"duration": 6.48342710037231,
"rev": "123",
"version": 4,
"message": "123",
"author": "vti",
"started": "2017-05-02 11:43:44.558950+0200"
}, ...]
"total": 5,
"pager": {
...
}
}

**Example**

$ curl http:https://localhost:5000/api/builds

#### Get Build

GET /builds/:uuid

**Response**

HTTP/1.1 200 Ok
Content-Type: application/json

{
"build" :
{
"status": "S",
"uuid": "d51ef218-2f1b-11e7-ab6d-4dcfdc676234",
"pid": 0,
"is_cancelable": "",
"created": "2017-05-02 11:43:44.430438+0200",
"finished": "2017-05-02 11:43:49.924477+0200",
"status_display": "success",
"is_new": "",
"branch": "master",
"project": "tu",
"is_restartable": "1",
"status_name": "Success",
"duration": 6.48342710037231,
"rev": "123",
"version": 4,
"message": "123",
"author": "vti",
"started": "2017-05-02 11:43:44.558950+0200"
}
}

**Example**

$ curl http:https://localhost:5000/api/builds

#### Create Build

POST /builds

**Content type**

Can be either `application/json` or `application/x-www-form-urlencoded`.

**Body params**

Required

- project=[string]
- rev=[string]
- branch=[string]
- author=[string]
- message=[string]

**Response**

HTTP/1.1 200 Ok
Content-Type: application/json

{"uuid":"d51ef218-2f1b-11e7-ab6d-4dcfdc676234"}

**Example**

$ curl http:https://localhost:5000/api/builds -d 'project=tu&rev=123&branch=master&author=vti&message=fix'

#### Cancel Build

POST /builds/:uuid/cancel

**Response**

HTTP/1.1 200 Ok
Content-Type: application/json

{"ok":1}

**Example**

$ curl http:https://localhost:5000/api/builds/d51ef218-2f1b-11e7-ab6d-4dcfdc676234/cancel

#### Restart Build

POST /builds/:uuid/restart

**Response**

HTTP/1.1 200 Ok
Content-Type: application/json

{"ok":1}

**Example**

$ curl http:https://localhost:5000/api/builds/d51ef218-2f1b-11e7-ab6d-4dcfdc676234/restart

### Build Logs

#### Download raw build log

GET /builds/:uuid/log

**Response**

HTTP/1.0 200 OK
Content-Type: text/plain
Content-Disposition: attachment; filename=6b90cf28-2f12-11e7-b73a-e1bddc676234.log

[...]

**Example**

$ curl http:https://localhost:5000/api/builds/d51ef218-2f1b-11e7-ab6d-4dcfdc676234/log

#### Watching the build log

GET /builds/:uuid/tail

**Content Type**

Output is in `text/event-stream` format. More info at
[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).

**Response**

HTTP/1.0 200 OK
Content-Type: text/event-stream; charset=UTF-8
Access-Control-Allow-Methods: GET
Access-Control-Allow-Credentials: true

data: [...]

**Example**

$ curl http:https://localhost:5000/api/builds/d51ef218-2f1b-11e7-ab6d-4dcfdc676234/tail

### Events

#### Watching events

GET /events

**Content Type**

Output is in `text/event-stream` format. More info at
[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).

**Response**

HTTP/1.0 200 OK
Content-Type: text/event-stream; charset=UTF-8
Access-Control-Allow-Methods: GET
Access-Control-Allow-Credentials: true

data: [...]

**Example**

$ curl http:https://localhost:5000/api/events

#### Create event

POST /events

**Response**

HTTP/1.0 200 OK
Content-Type: application/json

{"ok":1}

**Example**

$ curl http:https://localhost:5000/api/events -H 'Content-Type: application/json' -d '["event", {"data":"here"}]'

## Troubleshooting

Try *verbose* mode
Expand Down
1 change: 0 additions & 1 deletion bin/crafty
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ use Getopt::Long;
use Crafty;
use Crafty::Log;
use Crafty::Config;
use Crafty::EventBus;
use Crafty::PubSub;

my $opt_base = 'data';
Expand Down
2 changes: 0 additions & 2 deletions data/config.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@ pool:
mode: detach
projects:
- id: myapp
webhooks:
- provider: rest
build:
- sleep 5
26 changes: 16 additions & 10 deletions lib/Crafty.pm
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,21 @@ sub build_routes {

my $routes = Routes::Tiny->new;

$routes->add_route('/', name => 'Index');
$routes->add_route('/builds/:build_id', name => 'Build');
$routes->add_route('/tail/:build_id', name => 'Tail');
$routes->add_route('/cancel/:build_id', name => 'Cancel');
$routes->add_route('/download/:build_id', name => 'Download');
$routes->add_route('/restart/:build_id', name => 'Restart');

$routes->add_route('/events', name => 'Events');
$routes->add_route('/_event', name => 'Event');
$routes->add_route('/', method => 'GET', name => 'Index');
$routes->add_route('/builds/:uuid', method => 'GET', name => 'Build');
$routes->add_route('/cancel/:uuid', method => 'POST', name => 'Cancel');
$routes->add_route('/download/:uuid', method => 'GET', name => 'Download');
$routes->add_route('/restart/:uuid', method => 'POST', name => 'Restart');

$routes->add_route('/api/builds', method => 'POST', name => 'API::CreateBuild');
$routes->add_route('/api/builds', method => 'GET', name => 'API::ListBuilds');
$routes->add_route('/api/builds/:uuid', method => 'GET', name => 'API::GetBuild');
$routes->add_route('/api/builds/:uuid/cancel', method => 'POST', name => 'API::CancelBuild');
$routes->add_route('/api/builds/:uuid/restart', method => 'POST', name => 'API::RestartBuild');
$routes->add_route('/api/builds/:uuid/tail', method => 'GET', name => 'API::BuildTail');
$routes->add_route('/api/builds/:uuid/log', method => 'GET', name => 'API::BuildLog');
$routes->add_route('/api/events', method => 'GET', name => 'API::WatchEvents');
$routes->add_route('/api/events', method => 'POST', name => 'API::CreateEvent');

$routes->add_route('/webhook/:provider/:project', name => 'Hook');

Expand All @@ -74,7 +80,7 @@ sub to_psgi {

my $path_info = $env->{PATH_INFO};

my $match = $routes->match($path_info);
my $match = $routes->match($path_info, method => $env->{REQUEST_METHOD});

if ($match) {
my $action = $self->_build_action(
Expand Down
83 changes: 83 additions & 0 deletions lib/Crafty/Action/API/Base.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package Crafty::Action::API::Base;
use Moo;

use Plack::Request;
use JSON ();

has 'config', is => 'ro', required => 1;
has 'env', is => 'ro', required => 1;
has 'db', is => 'ro', required => 1;
has 'pool', is => 'ro';
has 'view', is => 'ro', required => 1;
has 'req',
is => 'ro',
lazy => 1,
default => sub {
my $self = shift;

return Plack::Request->new($self->env);
};

sub content_type { 'application/json' }

sub render {
my $self = shift;
my ($code, $body, $respond) = @_;

my $headers = [];

if ($self->content_type eq 'text/html') {
if ($body && ref $body && !$body->{ok}) {
my $template = lc((split /::/, ref($self))[-1]) . '.caml';

my $content = $self->view->render_file($template, $body);
$body = $self->view->render_file('layout.caml', { content => $content });
}
}
elsif ($self->content_type eq 'application/json') {
push @$headers, 'Content-Type' => 'application/json';

$body = JSON::encode_json($body);
}
else {
die 'Unknown content type';
}

my $res = [ $code, $headers, [$body] ];

return $respond ? $respond->($res) : $res;
}

sub not_found {
my $self = shift;
my ($respond) = @_;

return $self->render(404, { error => 'Not Found' }, $respond);
}

sub redirect {
my $self = shift;
my ($url, $respond) = @_;

my $res = [ 302, [ Location => $url ], [''] ];

return $respond ? $respond->($res) : $res;
}

sub handle_error {
my $self = shift;
my ($error, $respond) = @_;

Crafty::Log->error(@_) unless ref $error;

my $res;
eval { $res = ref $error ? $error : $self->render(500, { error => 'System error' }); } or do {
Crafty::Log->error($@);

$res = [ 500, [], ['System error'] ];
};

return $respond ? $respond->($res) : $res;
}

1;
Loading

0 comments on commit 743f43f

Please sign in to comment.