summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2013-10-09 04:14:12 -0700
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2013-10-09 04:14:12 -0700
commitc01896523ccb172cfe4b5e851f2d1e137b4963ed (patch)
tree95df87e15aa28de769c45e9d4e532ad22e59fa92
parent8ec401e0c6e360fd5150db2fad045909d89386e0 (diff)
parentb035790a8a39ec6f7d4c1d2f9f097dc9a8ce5aa4 (diff)
downloadgitlab-ci-c01896523ccb172cfe4b5e851f2d1e137b4963ed.tar.gz
Merge pull request #293 from alakra/extending_api
Extending API to manipulate Projects and Runners
-rw-r--r--.gitignore1
-rw-r--r--Gemfile.lock134
-rw-r--r--app/models/network.rb37
-rw-r--r--app/models/project.rb9
-rw-r--r--app/models/user_session.rb24
-rw-r--r--doc/api.md229
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/builds.rb8
-rw-r--r--lib/api/entities.rb8
-rw-r--r--lib/api/helpers.rb25
-rw-r--r--lib/api/projects.rb184
-rw-r--r--lib/api/runners.rb11
-rw-r--r--spec/helpers/application_helper_spec.rb68
-rw-r--r--spec/requests/projects_spec.rb176
14 files changed, 787 insertions, 128 deletions
diff --git a/.gitignore b/.gitignore
index bda868e..39a6f31 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ tmp/*
.powrc
.rvmrc
coverage/*
+.ruby-version
diff --git a/Gemfile.lock b/Gemfile.lock
index a5f9fad..3c90176 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -31,8 +31,8 @@ GEM
annotate (2.5.0)
rake
arel (3.0.2)
- backports (3.3.1)
- bootstrap-sass (2.3.1.3)
+ backports (3.3.4)
+ bootstrap-sass (2.3.2.2)
sass (~> 3.2)
builder (3.0.4)
capybara (2.1.0)
@@ -41,9 +41,10 @@ GEM
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
- celluloid (0.14.1)
- timers (>= 1.0.0)
- chronic (0.9.1)
+ celluloid (0.15.1)
+ timers (~> 1.1.0)
+ chronic (0.10.2)
+ cliver (0.2.2)
coderay (1.0.9)
coffee-rails (3.2.2)
coffee-script (>= 2.2.0)
@@ -51,31 +52,26 @@ GEM
coffee-script (2.2.0)
coffee-script-source
execjs
- coffee-script-source (1.6.2)
- colorize (0.5.8)
+ coffee-script-source (1.6.3)
connection_pool (1.1.0)
- coveralls (0.6.7)
- colorize
+ coveralls (0.7.0)
multi_json (~> 1.3)
rest-client
simplecov (>= 0.7)
+ term-ansicolor
thor
descendants_tracker (0.0.1)
diff-lcs (1.2.4)
- dotenv (0.7.0)
+ dotenv (0.9.0)
erubis (2.7.0)
- eventmachine (1.0.3)
- execjs (1.4.0)
- multi_json (~> 1.0)
+ execjs (2.0.1)
factory_girl (4.2.0)
activesupport (>= 3.0.0)
factory_girl_rails (4.2.1)
factory_girl (~> 4.2.0)
railties (>= 3.0.0)
- faye-websocket (0.4.7)
- eventmachine (>= 0.12.0)
- ffaker (1.16.1)
- ffi (1.8.1)
+ ffaker (1.18.0)
+ ffi (1.9.0)
font-awesome-sass-rails (3.0.2.2)
railties (>= 3.1.1)
sass-rails (>= 3.1.1)
@@ -84,7 +80,7 @@ GEM
thor (>= 0.13.6)
formatador (0.2.4)
gitlab_ci_meta (1.0)
- grape (0.4.1)
+ grape (0.6.0)
activesupport
builder
hashie (>= 1.2.0)
@@ -98,13 +94,13 @@ GEM
activesupport
multi_json (>= 1.3.2)
growl (1.0.3)
- guard (1.8.0)
+ guard (1.8.3)
formatador (>= 0.2.4)
- listen (>= 1.0.0)
+ listen (~> 1.3)
lumberjack (>= 1.0.2)
pry (>= 0.9.10)
thor (>= 0.14.6)
- guard-rspec (3.0.1)
+ guard-rspec (3.1.0)
guard (>= 1.8)
rspec (~> 2.13)
haml (4.0.3)
@@ -116,40 +112,42 @@ GEM
railties (>= 3.1, < 4.1)
hashie (2.0.5)
hike (1.2.3)
- http_parser.rb (0.5.3)
httparty (0.11.0)
multi_json (~> 1.0)
multi_xml (>= 0.5.2)
- i18n (0.6.4)
+ i18n (0.6.5)
journey (1.0.4)
- jquery-rails (3.0.0)
+ jquery-rails (3.0.4)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
json (1.8.0)
kaminari (0.14.1)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
- libv8 (3.11.8.17)
- listen (1.1.5)
+ libv8 (3.16.14.3)
+ listen (1.3.1)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
rb-kqueue (>= 0.2)
- lumberjack (1.0.3)
+ lumberjack (1.0.4)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
- method_source (0.8.1)
- mime-types (1.23)
+ method_source (0.8.2)
+ mime-types (1.25)
+ mini_portile (0.5.1)
minitest (4.3.2)
- multi_json (1.7.8)
- multi_xml (0.5.3)
- mysql2 (0.3.11)
- nokogiri (1.5.9)
- pg (0.15.1)
- poltergeist (1.3.0)
+ multi_json (1.8.0)
+ multi_xml (0.5.5)
+ mysql2 (0.3.13)
+ nokogiri (1.6.0)
+ mini_portile (~> 0.5.0)
+ pg (0.17.0)
+ poltergeist (1.4.1)
capybara (~> 2.1.0)
- faye-websocket (>= 0.4.4, < 0.5.0)
- http_parser.rb (~> 0.5.3)
+ cliver (~> 0.2.1)
+ multi_json (~> 1.0)
+ websocket-driver (>= 0.2.0)
polyglot (0.3.3)
pry (0.9.12.2)
coderay (~> 1.0.5)
@@ -189,7 +187,7 @@ GEM
thor (>= 0.14.6, < 2.0)
rake (10.1.0)
rb-fsevent (0.9.3)
- rb-inotify (0.9.0)
+ rb-inotify (0.9.2)
ffi (>= 0.5.0)
rb-kqueue (0.2.0)
ffi (>= 0.5.0)
@@ -201,47 +199,47 @@ GEM
ref (1.0.5)
rest-client (1.6.7)
mime-types (>= 1.16)
- rspec (2.13.0)
- rspec-core (~> 2.13.0)
- rspec-expectations (~> 2.13.0)
- rspec-mocks (~> 2.13.0)
- rspec-core (2.13.1)
- rspec-expectations (2.13.0)
+ rspec (2.14.1)
+ rspec-core (~> 2.14.0)
+ rspec-expectations (~> 2.14.0)
+ rspec-mocks (~> 2.14.0)
+ rspec-core (2.14.5)
+ rspec-expectations (2.14.3)
diff-lcs (>= 1.1.3, < 2.0)
- rspec-mocks (2.13.1)
- rspec-rails (2.13.2)
+ rspec-mocks (2.14.3)
+ rspec-rails (2.14.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
- rspec-core (~> 2.13.0)
- rspec-expectations (~> 2.13.0)
- rspec-mocks (~> 2.13.0)
- sass (3.2.9)
+ rspec-core (~> 2.14.0)
+ rspec-expectations (~> 2.14.0)
+ rspec-mocks (~> 2.14.0)
+ sass (3.2.10)
sass-rails (3.2.6)
railties (~> 3.2.0)
sass (>= 3.1.10)
tilt (~> 1.3)
settingslogic (2.0.9)
- shoulda-matchers (2.1.0)
+ shoulda-matchers (2.4.0)
activesupport (>= 3.0.0)
- sidekiq (2.14.0)
+ sidekiq (2.14.1)
celluloid (>= 0.14.1)
connection_pool (>= 1.0.0)
json
redis (>= 3.0.4)
- redis-namespace
+ redis-namespace (>= 1.3.1)
simplecov (0.7.1)
multi_json (~> 1.0)
simplecov-html (~> 0.7.1)
simplecov-html (0.7.1)
- sinatra (1.3.6)
+ sinatra (1.4.3)
rack (~> 1.4)
- rack-protection (~> 1.3)
- tilt (~> 1.3, >= 1.3.3)
- slim (2.0.0)
- temple (~> 0.6.5)
- tilt (~> 1.3, >= 1.3.3)
- slop (3.4.5)
+ rack-protection (~> 1.4)
+ tilt (~> 1.3, >= 1.3.4)
+ slim (2.0.1)
+ temple (~> 0.6.6)
+ tilt (>= 1.3.3, < 2.1)
+ slop (3.4.6)
sprockets (2.2.2)
hike (~> 1.2)
multi_json (~> 1.0)
@@ -249,24 +247,28 @@ GEM
tilt (~> 1.1, != 1.3.0)
stamp (0.5.0)
state_machine (1.2.0)
- temple (0.6.5)
- therubyracer (0.11.4)
- libv8 (~> 3.11.8.12)
+ temple (0.6.6)
+ term-ansicolor (1.2.2)
+ tins (~> 0.8)
+ therubyracer (0.12.0)
+ libv8 (~> 3.16.14.0)
ref
thor (0.18.1)
tilt (1.4.1)
timers (1.1.0)
- treetop (1.4.14)
+ tins (0.10.0)
+ treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
tzinfo (0.3.37)
- uglifier (2.1.1)
+ uglifier (2.2.1)
execjs (>= 0.3.0)
multi_json (~> 1.0, >= 1.0.2)
virtus (0.5.5)
backports (~> 3.3)
descendants_tracker (~> 0.0.1)
- whenever (0.8.2)
+ websocket-driver (0.3.0)
+ whenever (0.8.4)
activesupport (>= 2.3.4)
chronic (>= 0.6.3)
xpath (2.0.0)
diff --git a/app/models/network.rb b/app/models/network.rb
index edad754..7a17416 100644
--- a/app/models/network.rb
+++ b/app/models/network.rb
@@ -1,13 +1,16 @@
class Network
include HTTParty
+ API_PREFIX = '/api/v3/'
+
def authenticate(url, api_opts)
opts = {
body: api_opts.to_json,
headers: {"Content-Type" => "application/json"},
}
- response = self.class.post(url + api_prefix + 'session.json', opts)
+ endpoint = File.join(url, API_PREFIX, 'session.json')
+ response = self.class.post(endpoint, opts)
if response.code == 201
response.parsed_response
@@ -16,6 +19,23 @@ class Network
end
end
+ def authenticate_by_token(url, api_opts)
+ opts = {
+ query: api_opts,
+ headers: {"Content-Type" => "application/json"},
+ }
+
+ endpoint = File.join(url, API_PREFIX, 'user.json')
+ response = self.class.get(endpoint, opts)
+
+ if response.code == 200
+ response.parsed_response
+ else
+ nil
+ end
+ end
+
+
def projects(url, api_opts, scope = :owned)
opts = {
query: api_opts,
@@ -28,7 +48,8 @@ class Network
'projects.json'
end
- response = self.class.get(url + api_prefix + query, opts)
+ endpoint = File.join(url, API_PREFIX, query)
+ response = self.class.get(endpoint, opts)
if response.code == 200
response.parsed_response
@@ -45,7 +66,8 @@ class Network
query = "projects/#{project_id}.json"
- response = self.class.get(url + api_prefix + query, opts)
+ endpoint = File.join(url, API_PREFIX, query)
+ response = self.class.get(endpoint, opts)
if response.code == 200
response.parsed_response
@@ -60,7 +82,8 @@ class Network
headers: {"Content-Type" => "application/json"},
}
- response = self.class.post(url + api_prefix + "projects/#{project_id}/keys.json", opts)
+ endpoint = File.join(url, API_PREFIX, "projects/#{project_id}/keys.json")
+ response = self.class.post(endpoint, opts)
if response.code == 201
response.parsed_response
@@ -68,10 +91,4 @@ class Network
nil
end
end
-
- private
-
- def api_prefix
- '/api/v3/'
- end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index b9de85c..e324dc5 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -27,7 +27,6 @@ class Project < ActiveRecord::Base
has_many :runner_projects, dependent: :destroy
has_many :runners, through: :runner_projects
-
#
# Validations
#
@@ -45,11 +44,9 @@ class Project < ActiveRecord::Base
before_validation :set_default_values
def self.from_gitlab(user, page, per_page, scope = :owned)
- opts = {
- private_token: user.private_token,
- per_page: per_page,
- page: page,
- }
+ opts = { private_token: user.private_token }
+ opts[:per_page] = per_page if per_page.present?
+ opts[:page] = page if page.present?
projects = Network.new.projects(user.url, opts, scope)
diff --git a/app/models/user_session.rb b/app/models/user_session.rb
index 8e0376f..3d3a273 100644
--- a/app/models/user_session.rb
+++ b/app/models/user_session.rb
@@ -5,15 +5,31 @@ class UserSession
attr_accessor :email, :password, :url
- def authenticate auth_opts
- url = auth_opts.delete(:url)
+ def authenticate(auth_opts)
+ authenticate_via(auth_opts) do |url, network, options|
+ network.authenticate(url, options)
+ end
+ end
+
+ def authenticate_by_token(auth_opts)
+ result = authenticate_via(auth_opts) do |url, network, options|
+ network.authenticate_by_token(url, options)
+ end
+
+ result
+ end
+
+ private
+
+ def authenticate_via(options, &block)
+ url = options.delete(:url)
return nil unless GitlabCi.config.allowed_gitlab_urls.include?(url)
- user = Network.new.authenticate(url, auth_opts)
+ user = block.call(url, Network.new, options)
if user
- User.new(user.merge("url" => url))
+ return User.new(user.merge({"url" => url}))
else
nil
end
diff --git a/doc/api.md b/doc/api.md
new file mode 100644
index 0000000..1472918
--- /dev/null
+++ b/doc/api.md
@@ -0,0 +1,229 @@
+# Gitlab CI API
+
+This API is intended to aid in the setup and configuration of
+projects, builds and runners on Gitlab CI. Authentication is done by
+sending the `private-token` of a valid user and the `url` of an
+authorized Gitlab instance via a query string along with the API
+request:
+
+ GET http://ci.example.com/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/
+
+If preferred, you may instead send the `private-token` as a header in
+your request:
+
+ curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://ci.example.com/api/v1/projects?url=http://demo.gitlab.com/"
+
+All API requests are serialized using JSON. You don't need to specify
+`.json` at the end of API URL.
+
+# API Requests
+
+This lists all the requests that can be made via the API.
+
+## Projects
+
+### List Authorized Projects
+
+Lists all projects that the authenticated user has access to.
+
+```
+GET /projects
+```
+
+Returns:
+
+```json
+ [
+ {
+ "id" : 271,
+ "name" : "gitlabhq",
+ "timeout" : 1800,
+ "scripts" : "ls",
+ "token" : "iPWx6WM4lhHNedGfBpPJNP",
+ "default_ref" : "master",
+ "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
+ "always_build" : false,
+ "polling_interval" : null,
+ "public" : false,
+ "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "gitlab_id" : 3
+ },
+ {
+ "id" : 272,
+ "name" : "gitlab-ci",
+ "timeout" : 1800,
+ "scripts" : "ls",
+ "token" : "iPWx6WM4lhHNedGfBpPJNP",
+ "default_ref" : "master",
+ "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
+ "always_build" : false,
+ "polling_interval" : null,
+ "public" : false,
+ "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "gitlab_id" : 4
+ }
+]
+```
+
+### List Owned Projects
+
+Lists all projects that the authenticated user owns.
+
+```
+GET /projects/owned
+```
+
+Returns:
+
+```json
+[
+ {
+ "id" : 272,
+ "name" : "gitlab-ci",
+ "timeout" : 1800,
+ "scripts" : "ls",
+ "token" : "iPWx6WM4lhHNedGfBpPJNP",
+ "default_ref" : "master",
+ "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
+ "always_build" : false,
+ "polling_interval" : null,
+ "public" : false,
+ "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "gitlab_id" : 4
+ }
+]
+```
+
+### Single Project
+
+Returns information about a single project for which the user is
+authorized.
+
+ GET /projects/:id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+
+### Create Project
+
+Creates a Gitlab CI project using Gitlab project details.
+
+ POST /projects/:id
+
+Parameters:
+
+ * `name` (required) - The name of the project
+ * `gitlab_id` (required) - The ID of the project on the Gitlab instance
+ * `gitlab_url` (required) - The web url of the project on the Gitlab instance
+ * `ssh_url_to_repo` (required) - The gitlab SSH url to the repo
+ * `scripts` (optional) - The shell script provided for a runner to run (defaults to `ls -al`)
+ * `default_ref` (optional) - The branch to run on (default to `master`)
+
+### Update Project
+
+Updates a Gitlab CI project using Gitlab project details that the
+authenticated user has access to.
+
+ PUT /projects/:id
+
+Parameters:
+
+ * `name` - The name of the project
+ * `gitlab_id` - The ID of the project on the Gitlab instance
+ * `gitlab_url` - The web url of the project on the Gitlab instance
+ * `ssh_url_to_repo` - The gitlab SSH url to the repo
+ * `scripts` - The shell script provided for a runner to run (defaults to `ls -al`)
+ * `default_ref` - The branch to run on (default to `master`)
+
+### Remove Project
+
+Removes a Gitlab CI project that the authenticated user has access to.
+
+ DELETE /projects/:id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+
+### Link Project to Runner
+
+Links a runner to a project so that it can make builds (only via
+authorized user).
+
+ POST /projects/:id/runners/:runner_id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+ * `runner_id` (required) - The ID of the Gitlab CI runner
+
+### Remove Project from Runner
+
+Removes a runner from a project so that it can not make builds (only
+via authorized user).
+
+ DELETE /projects/:id/runners/:runner_id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+ * `runner_id` (required) - The ID of the Gitlab CI runner
+
+## Runners
+
+### Register a new runner
+
+Used to make Gitlab CI aware of available runners.
+
+ POST /runners/register
+
+Parameters:
+
+ * `token` (required) - The unique token of runner
+ * `public_key` (required) - Deploy key used to get projects
+
+Returns:
+
+```json
+{
+ "id" : 85,
+ "token" : "12b68e90394084703135"
+}
+```
+
+## Builds
+
+### Runs oldest pending build by runner
+
+ POST /builds/register
+
+Parameters:
+
+ * `token` (required) - The unique token of runner
+
+Returns:
+
+```json
+{
+ "id" : 79,
+ "commands" : "",
+ "path" : "",
+ "ref" : "",
+ "sha" : "",
+ "project_id" : 6,
+ "repo_url" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "before_sha" : ""
+}
+```
+
+
+### Update details of an existing build
+
+ PUT /builds/:id
+
+Parameters:
+
+ * `id` (required) - The ID of a project
+ * `state` (optional) - The state of a build
+ * `trace` (optional) - The trace of a build
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 394da09..f41a881 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -27,5 +27,6 @@ module API
mount Builds
mount Runners
+ mount Projects
end
end
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index 75ae4e0..3b70a88 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -1,10 +1,10 @@
module API
- # Issues API
+ # Builds API
class Builds < Grape::API
resource :builds do
- before { authenticate_runner!}
+ before { authenticate_runner! }
- # Register a build by runner
+ # Runs oldest pending build by runner
#
# Parameters:
# token (required) - The uniq token of runner
@@ -28,7 +28,7 @@ module API
# Parameters:
# id (required) - The ID of a project
# state (optional) - The state of a build
- # output (optional) - The trace of a build
+ # trace (optional) - The trace of a build
# Example Request:
# PUT /builds/:id
put ":id" do
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index bdc9e09..3f65a44 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -7,5 +7,13 @@ module API
class Runner < Grape::Entity
expose :id, :token
end
+
+ class Project < Grape::Entity
+ expose :id, :name, :timeout, :scripts, :token, :default_ref, :gitlab_url, :always_build, :polling_interval, :public, :ssh_url_to_repo, :gitlab_id
+ end
+
+ class RunnerProject < Grape::Entity
+ expose :id, :project_id, :runner_id
+ end
end
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index f76b039..4bea5a2 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -1,5 +1,26 @@
module API
module Helpers
+ PRIVATE_TOKEN_PARAM = :private_token
+ PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
+
+ def current_user
+ @current_user ||= begin
+ options = {
+ :private_token => (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]),
+ :url => params[:url]
+ }
+ UserSession.new.authenticate_by_token(options)
+ end
+ end
+
+ def current_runner
+ @runner ||= Runner.find_by_token(params[:token])
+ end
+
+ def authenticate!
+ forbidden! unless current_user
+ end
+
def authenticate_runners!
forbidden! unless params[:token] == GitlabCi::RunnersToken
end
@@ -8,10 +29,6 @@ module API
forbidden! unless current_runner
end
- def current_runner
- @runner ||= Runner.find_by_token(params[:token])
- end
-
# Checks the occurrences of required attributes, each attribute must be present in the params hash
# or a Bad Request error is invoked.
#
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
new file mode 100644
index 0000000..f2334a5
--- /dev/null
+++ b/lib/api/projects.rb
@@ -0,0 +1,184 @@
+module API
+ # Projects API
+ class Projects < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ # Retrieve all Gitlab CI projects that the user has access to
+ #
+ # Example Request:
+ # GET /projects
+ get do
+ gitlab_projects = Project.from_gitlab(current_user, nil, nil, :authorized)
+ ids = gitlab_projects.map { |project| project.id }
+
+ projects = Project.where("gitlab_id IN (?)", ids).all
+ present projects, with: Entities::Project
+ end
+
+ # Retrieve all Gitlab CI projects that the user owns
+ #
+ # Example Request:
+ # GET /projects/owned
+ get "owned" do
+ gitlab_projects = Project.from_gitlab(current_user, nil, nil, :owned)
+ ids = gitlab_projects.map { |project| project.id }
+
+ projects = Project.where("gitlab_id IN (?)", ids).all
+ present projects, with: Entities::Project
+ end
+
+ # Retrieve info for a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # GET /projects/:id
+ get ":id" do
+ project = Project.find(params[:id])
+
+ if current_user.can_access_project?(project.gitlab_id)
+ present project, with: Entities::Project
+ else
+ unauthorized!
+ end
+ end
+
+ # Create Gitlab CI project using Gitlab project info
+ #
+ # Parameters:
+ # name (required) - The name of the project
+ # gitlab_id (required) - The gitlab id of the project
+ # gitlab_url (required) - The gitlab web url to the project
+ # ssh_url_to_repo (required) - The gitlab ssh url to the repo
+ # scripts - The shell script provided for a runner to run
+ # default_ref - The branch to run against (defaults to `master`)
+ # Example Request:
+ # POST /projects
+ post do
+ required_attributes! [:name, :gitlab_id, :gitlab_url, :ssh_url_to_repo]
+
+ filtered_params = {
+ :name => params[:name],
+ :gitlab_id => params[:gitlab_id],
+ :gitlab_url => params[:gitlab_url],
+ :scripts => params[:scripts] || 'ls -al',
+ :default_ref => params[:default_ref] || 'master',
+ :ssh_url_to_repo => params[:ssh_url_to_repo]
+ }
+
+ project = Project.new(filtered_params)
+
+ if project.save
+ present project, :with => Entities::Project
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Update a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # name - The name of the project
+ # gitlab_id - The gitlab id of the project
+ # gitlab_url - The gitlab web url to the project
+ # ssh_url_to_repo - The gitlab ssh url to the repo
+ # scripts - The shell script provided for a runner to run
+ # default_ref - The branch to run against (defaults to `master`)
+ # Example Request:
+ # PUT /projects/:id
+ put ":id" do
+ project = Project.find(params[:id])
+
+ if project.present? && current_user.can_access_project?(project.gitlab_id)
+ attrs = attributes_for_keys [:name, :gitlab_id, :gitlab_url, :scripts, :default_ref, :ssh_url_to_repo]
+
+ if project.update_attributes(attrs)
+ present project, :with => Entities::Project
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ else
+ not_found!
+ end
+ end
+
+ # Remove a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # DELETE /projects/:id
+ delete ":id" do
+ project = Project.find(params[:id])
+
+ if project.present? && current_user.can_access_project?(project.gitlab_id)
+ project.destroy
+ else
+ not_found!
+ end
+ end
+
+ # Link a Gitlab CI project to a runner
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # runner_id (required) - The ID of a runner
+ # Example Request:
+ # POST /projects/:id/runners/:runner_id
+ post ":id/runners/:runner_id" do
+ project = Project.find_by_id(params[:id])
+ runner = Runner.find_by_id(params[:runner_id])
+
+ not_found! if project.blank? or runner.blank?
+
+ unauthorized! unless current_user.can_access_project?(project.gitlab_id)
+
+ options = {
+ :project_id => project.id,
+ :runner_id => runner.id
+ }
+
+ runner_project = RunnerProject.new(options)
+
+ if runner_project.save
+ present runner_project, :with => Entities::RunnerProject
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Remove a Gitlab CI project from a runner
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # runner_id (required) - The ID of a runner
+ # Example Request:
+ # DELETE /projects/:id/runners/:runner_id
+ delete ":id/runners/:runner_id" do
+ project = Project.find_by_id(params[:id])
+ runner = Runner.find_by_id(params[:runner_id])
+
+ not_found! if project.blank? or runner.blank?
+ unauthorized! unless current_user.can_access_project?(project.gitlab_id)
+
+ options = {
+ :project_id => project.id,
+ :runner_id => runner.id
+ }
+
+ runner_project = RunnerProject.where(options).first
+
+ if runner_project.present?
+ runner_project.destroy
+ else
+ not_found!
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index ee7b343..8bf2370 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -1,16 +1,17 @@
module API
- # Issues API
+ # Runners API
class Runners < Grape::API
resource :runners do
- before { authenticate_runners!}
+ before { authenticate_runners! }
- # Register a build by runner
+ # Register a new runner
#
# Parameters:
- # token (required) - The uniq token of runner
+ # token (required) - The unique token of runner
+ # public_key (required) - Deploy key used to get projects
#
# Example Request:
- # POST /builds/register
+ # POST /runners/register
post "register" do
required_attributes! [:token, :public_key]
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index a452278..bc6aa3b 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -13,35 +13,45 @@ describe ApplicationHelper do
gravatar_icon('').should == 'no_avatar.png'
end
- it "should return default gravatar url" do
- stub!(:request).and_return(double(:ssl? => false))
- gravatar_icon(user_email).should match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118')
- end
-
- it "should use SSL when appropriate" do
- stub!(:request).and_return(double(:ssl? => true))
- gravatar_icon(user_email).should match('https://secure.gravatar.com')
- end
-
- it "should return custom gravatar path when gravatar_url is set" do
- stub!(:request).and_return(double(:ssl? => false))
- GitlabCi.config.gravatar.stub(:plain_url).and_return('http://example.local/?s=%{size}&hash=%{hash}')
- gravatar_icon(user_email, 20).should == 'http://example.local/?s=20&hash=b58c6f14d292556214bd64909bcdb118'
- end
-
- it "should accept a custom size" do
- stub!(:request).and_return(double(:ssl? => false))
- gravatar_icon(user_email, 64).should match(/\?s=64/)
- end
-
- it "should use default size when size is wrong" do
- stub!(:request).and_return(double(:ssl? => false))
- gravatar_icon(user_email, nil).should match(/\?s=40/)
- end
-
- it "should be case insensitive" do
- stub!(:request).and_return(double(:ssl? => false))
- gravatar_icon(user_email).should == gravatar_icon(user_email.upcase + " ")
+ context "with no ssl" do
+ let!(:request) {
+ request_double = double("Request")
+ request_double.stub(:ssl?).and_return(false)
+ request_double
+ }
+
+ it "should return default gravatar url" do
+ gravatar_icon(user_email).should match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118')
+ end
+
+ it "should return custom gravatar path when gravatar_url is set" do
+ GitlabCi.config.gravatar.stub(:plain_url).and_return('http://example.local/?s=%{size}&hash=%{hash}')
+ gravatar_icon(user_email, 20).should == 'http://example.local/?s=20&hash=b58c6f14d292556214bd64909bcdb118'
+ end
+
+ it "should accept a custom size" do
+ gravatar_icon(user_email, 64).should match(/\?s=64/)
+ end
+
+ it "should use default size when size is wrong" do
+ gravatar_icon(user_email, nil).should match(/\?s=40/)
+ end
+
+ it "should be case insensitive" do
+ gravatar_icon(user_email).should == gravatar_icon(user_email.upcase + " ")
+ end
+ end
+
+ context "with ssl" do
+ let!(:request) {
+ request_double = double("Request")
+ request_double.stub(:ssl?).and_return(true)
+ request_double
+ }
+
+ it "should use SSL when appropriate" do
+ gravatar_icon(user_email).should match('https://secure.gravatar.com')
+ end
end
end
end
diff --git a/spec/requests/projects_spec.rb b/spec/requests/projects_spec.rb
new file mode 100644
index 0000000..ba7e8ee
--- /dev/null
+++ b/spec/requests/projects_spec.rb
@@ -0,0 +1,176 @@
+require 'spec_helper'
+
+describe API::API do
+ include ApiHelpers
+
+ let(:gitlab_url) { GitlabCi.config.allowed_gitlab_urls.first }
+ let(:auth_opts) {
+ {
+ :email => "test@test.com",
+ :password => "123456"
+ }
+ }
+
+ let(:private_token) { Network.new.authenticate(gitlab_url, auth_opts)["private_token"] }
+ let(:options) {
+ {
+ :private_token => private_token,
+ :url => gitlab_url
+ }
+ }
+
+ context "requests for scoped projects" do
+ # NOTE: These ids are tied to the actual projects on demo.gitlab.com
+ describe "GET /projects" do
+ let!(:project1) { FactoryGirl.create(:project, :name => "gitlabhq", :gitlab_id => 3) }
+ let!(:project2) { FactoryGirl.create(:project, :name => "gitlab-ci", :gitlab_id => 4) }
+
+ it "should return all projects on the CI instance" do
+ get api("/projects"), options
+ response.status.should == 200
+ json_response.count.should == 2
+ json_response.first["id"].should == project1.id
+ json_response.last["id"].should == project2.id
+ end
+ end
+
+ describe "GET /projects/owned" do
+ # NOTE: This user doesn't own any of these projects on demo.gitlab.com
+ let!(:project1) { FactoryGirl.create(:project, :name => "gitlabhq", :gitlab_id => 3) }
+ let!(:project2) { FactoryGirl.create(:project, :name => "random-project", :gitlab_id => 9898) }
+
+ it "should return all projects on the CI instance" do
+ get api("/projects/owned"), options
+
+ response.status.should == 200
+ json_response.count.should == 0
+ end
+ end
+ end
+
+
+ describe "GET /projects/:id" do
+ let!(:project) { FactoryGirl.create(:project) }
+
+ context "with an existing project" do
+ it "should retrieve the project info" do
+ get api("/projects/#{project.id}"), options
+ response.status.should == 200
+ json_response['id'].should == project.id
+ end
+ end
+
+ context "with a non-existing project" do
+ it "should return 404 error if project not found" do
+ get api("/projects/non_existent_id"), options
+ response.status.should == 404
+ end
+ end
+ end
+
+ describe "PUT /projects/:id" do
+ let!(:project) { FactoryGirl.create(:project) }
+ let!(:project_info) { {:name => "An updated name!" } }
+
+ before do
+ options.merge!(project_info)
+ end
+
+ it "should update a specific project's information" do
+ put api("/projects/#{project.id}"), options
+ response.status.should == 200
+ json_response["name"].should == project_info[:name]
+ end
+
+ it "fails to update a non-existing project" do
+ put api("/projects/non-existant-id"), options
+ response.status.should == 404
+ end
+ end
+
+ describe "DELETE /projects/:id" do
+ let!(:project) { FactoryGirl.create(:project) }
+
+ it "should delete a specific project" do
+ delete api("/projects/#{project.id}"), options
+ response.status.should == 200
+
+ expect { project.reload }.to raise_error
+ end
+ end
+
+ describe "POST /projects" do
+ let(:project_info) {
+ {
+ :name => "My project",
+ :gitlab_id => 1,
+ :gitlab_url => "http://example.com/testing/testing",
+ :ssh_url_to_repo => "ssh://example.com/testing/testing.git"
+ }
+ }
+
+ let(:invalid_project_info) { {} }
+
+ context "with valid project info" do
+ before do
+ options.merge!(project_info)
+ end
+
+ it "should create a project with valid data" do
+ post api("/projects"), options
+ response.status.should == 201
+ json_response['name'].should == project_info[:name]
+ end
+ end
+
+ context "with invalid project info" do
+ before do
+ options.merge!(invalid_project_info)
+ end
+
+ it "should error with invalid data" do
+ post api("/projects"), options
+ response.status.should == 400
+ end
+ end
+
+ describe "POST /projects/:id/runners/:id" do
+ let(:project) { FactoryGirl.create(:project) }
+ let(:runner) { FactoryGirl.create(:runner) }
+
+ it "should add the project to the runner" do
+ post api("/projects/#{project.id}/runners/#{runner.id}"), options
+ response.status.should == 201
+
+ project.reload
+ project.runners.first.id.should == runner.id
+ end
+
+ it "should fail if it tries to link a non-existing project or runner" do
+ post api("/projects/#{project.id}/runners/non-existing"), options
+ response.status.should == 404
+
+ post api("/projects/non-existing/runners/#{runner.id}"), options
+ response.status.should == 404
+ end
+ end
+
+ describe "DELETE /projects/:id/runners/:id" do
+ let(:project) { FactoryGirl.create(:project) }
+ let(:runner) { FactoryGirl.create(:runner) }
+
+ before do
+ post api("/projects/#{project.id}/runners/#{runner.id}"), options
+ end
+
+ it "should remove the project from the runner" do
+ project.runners.should be_present
+ delete api("/projects/#{project.id}/runners/#{runner.id}"), options
+ response.status.should == 200
+
+ project.reload
+ project.runners.should be_empty
+ end
+ end
+ end
+end