diff options
author | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2013-10-09 04:14:12 -0700 |
---|---|---|
committer | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2013-10-09 04:14:12 -0700 |
commit | c01896523ccb172cfe4b5e851f2d1e137b4963ed (patch) | |
tree | 95df87e15aa28de769c45e9d4e532ad22e59fa92 | |
parent | 8ec401e0c6e360fd5150db2fad045909d89386e0 (diff) | |
parent | b035790a8a39ec6f7d4c1d2f9f097dc9a8ce5aa4 (diff) | |
download | gitlab-ci-c01896523ccb172cfe4b5e851f2d1e137b4963ed.tar.gz |
Merge pull request #293 from alakra/extending_api
Extending API to manipulate Projects and Runners
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Gemfile.lock | 134 | ||||
-rw-r--r-- | app/models/network.rb | 37 | ||||
-rw-r--r-- | app/models/project.rb | 9 | ||||
-rw-r--r-- | app/models/user_session.rb | 24 | ||||
-rw-r--r-- | doc/api.md | 229 | ||||
-rw-r--r-- | lib/api/api.rb | 1 | ||||
-rw-r--r-- | lib/api/builds.rb | 8 | ||||
-rw-r--r-- | lib/api/entities.rb | 8 | ||||
-rw-r--r-- | lib/api/helpers.rb | 25 | ||||
-rw-r--r-- | lib/api/projects.rb | 184 | ||||
-rw-r--r-- | lib/api/runners.rb | 11 | ||||
-rw-r--r-- | spec/helpers/application_helper_spec.rb | 68 | ||||
-rw-r--r-- | spec/requests/projects_spec.rb | 176 |
14 files changed, 787 insertions, 128 deletions
@@ -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 |