Merge branch 'ci/api-runners' into 'master'
Add runners API References #4264 See merge request !2640
This commit is contained in:
commit
1817b766b2
14 changed files with 1008 additions and 26 deletions
|
@ -65,6 +65,7 @@ v 8.5.0 (unreleased)
|
|||
- Ability to see and sort on vote count from Issues and MR lists
|
||||
- Fix builds scheduler when first build in stage was allowed to fail
|
||||
- User project limit is reached notice is hidden if the projects limit is zero
|
||||
- Add API support for managing runners and project's runners
|
||||
|
||||
v 8.4.4
|
||||
- Update omniauth-saml gem to 1.4.2
|
||||
|
|
|
@ -22,6 +22,7 @@ module Ci
|
|||
extend Ci::Model
|
||||
|
||||
LAST_CONTACT_TIME = 5.minutes.ago
|
||||
AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online']
|
||||
|
||||
has_many :builds, class_name: 'Ci::Build'
|
||||
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
|
||||
|
@ -38,6 +39,11 @@ module Ci
|
|||
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
|
||||
scope :ordered, ->() { order(id: :desc) }
|
||||
|
||||
scope :owned_or_shared, ->(project_id) do
|
||||
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
|
||||
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
|
||||
end
|
||||
|
||||
acts_as_taggable
|
||||
|
||||
def self.search(query)
|
||||
|
|
|
@ -32,6 +32,7 @@ following locations:
|
|||
- [Builds](builds.md)
|
||||
- [Build triggers](build_triggers.md)
|
||||
- [Build Variables](build_variables.md)
|
||||
- [Runners](runners.md)
|
||||
|
||||
## Authentication
|
||||
|
||||
|
|
318
doc/api/runners.md
Normal file
318
doc/api/runners.md
Normal file
|
@ -0,0 +1,318 @@
|
|||
# Runners API
|
||||
|
||||
## List owned runners
|
||||
|
||||
Get a list of specific runners available to the user.
|
||||
|
||||
```
|
||||
GET /runners
|
||||
GET /runners?scope=active
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|---------|----------|---------------------|
|
||||
| `scope` | string | no | The scope of specific runners to show, one of: `active`, `paused`, `online`; showing all runners if none provided |
|
||||
|
||||
```
|
||||
curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"active": true,
|
||||
"description": "test-1-20150125",
|
||||
"id": 6,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"description": "test-2-20150125",
|
||||
"id": 8,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## List all runners
|
||||
|
||||
Get a list of all runners in the GitLab instance (specific and shared). Access
|
||||
is restricted to users with `admin` privileges.
|
||||
|
||||
```
|
||||
GET /runners/all
|
||||
GET /runners/all?scope=online
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|---------|----------|---------------------|
|
||||
| `scope` | string | no | The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`; showing all runners if none provided |
|
||||
|
||||
```
|
||||
curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"active": true,
|
||||
"description": "shared-runner-1",
|
||||
"id": 1,
|
||||
"is_shared": true,
|
||||
"name": null
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"description": "shared-runner-2",
|
||||
"id": 3,
|
||||
"is_shared": true,
|
||||
"name": null
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"description": "test-1-20150125",
|
||||
"id": 6,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"description": "test-2-20150125",
|
||||
"id": 8,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Get runner's details
|
||||
|
||||
Get details of a runner.
|
||||
|
||||
```
|
||||
GET /runners/:id
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|---------|----------|---------------------|
|
||||
| `id` | integer | yes | The ID of a runner |
|
||||
|
||||
```
|
||||
curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"active": true,
|
||||
"architecture": null,
|
||||
"description": "test-1-20150125",
|
||||
"id": 6,
|
||||
"is_shared": false,
|
||||
"contacted_at": "2016-01-25T16:39:48.066Z",
|
||||
"name": null,
|
||||
"platform": null,
|
||||
"projects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "GitLab Community Edition",
|
||||
"name_with_namespace": "GitLab.org / GitLab Community Edition",
|
||||
"path": "gitlab-ce",
|
||||
"path_with_namespace": "gitlab-org/gitlab-ce"
|
||||
}
|
||||
],
|
||||
"token": "205086a8e3b9a2b818ffac9b89d102",
|
||||
"revision": null,
|
||||
"tag_list": [
|
||||
"ruby",
|
||||
"mysql"
|
||||
],
|
||||
"version": null
|
||||
}
|
||||
```
|
||||
|
||||
## Update runner's details
|
||||
|
||||
Update details of a runner.
|
||||
|
||||
```
|
||||
PUT /runners/:id
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|---------------|---------|----------|---------------------|
|
||||
| `id` | integer | yes | The ID of a runner |
|
||||
| `description` | string | no | The description of a runner |
|
||||
| `active` | boolean | no | The state of a runner; can be set to `true` or `false` |
|
||||
| `tag_list` | array | no | The list of tags for a runner; put array of tags, that should be finally assigned to a runner |
|
||||
|
||||
```
|
||||
curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" -F "description=test-1-20150125-test" -F "tag_list=ruby,mysql,tag1,tag2"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"active": true,
|
||||
"architecture": null,
|
||||
"description": "test-1-20150125-test",
|
||||
"id": 6,
|
||||
"is_shared": false,
|
||||
"contacted_at": "2016-01-25T16:39:48.066Z",
|
||||
"name": null,
|
||||
"platform": null,
|
||||
"projects": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "GitLab Community Edition",
|
||||
"name_with_namespace": "GitLab.org / GitLab Community Edition",
|
||||
"path": "gitlab-ce",
|
||||
"path_with_namespace": "gitlab-org/gitlab-ce"
|
||||
}
|
||||
],
|
||||
"token": "205086a8e3b9a2b818ffac9b89d102",
|
||||
"revision": null,
|
||||
"tag_list": [
|
||||
"ruby",
|
||||
"mysql",
|
||||
"tag1",
|
||||
"tag2"
|
||||
],
|
||||
"version": null
|
||||
}
|
||||
```
|
||||
|
||||
## Remove a runner
|
||||
|
||||
Remove a runner.
|
||||
|
||||
```
|
||||
DELETE /runners/:id
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|---------|----------|---------------------|
|
||||
| `id` | integer | yes | The ID of a runner |
|
||||
|
||||
```
|
||||
curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"active": true,
|
||||
"description": "test-1-20150125-test",
|
||||
"id": 6,
|
||||
"is_shared": false,
|
||||
"name": null,
|
||||
}
|
||||
```
|
||||
|
||||
## List project's runners
|
||||
|
||||
List all runners (specific and shared) available in the project. Shared runners
|
||||
are listed if at least one shared runner is defined **and** shared runners
|
||||
usage is enabled in the project's settings.
|
||||
|
||||
```
|
||||
GET /projects/:id/runners
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|---------|----------|---------------------|
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
|
||||
```
|
||||
curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"active": true,
|
||||
"description": "test-2-20150125",
|
||||
"id": 8,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"description": "development_runner",
|
||||
"id": 5,
|
||||
"is_shared": true,
|
||||
"name": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Enable a runner in project
|
||||
|
||||
Enable an available specific runner in the project.
|
||||
|
||||
```
|
||||
POST /projects/:id/runners
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-------------|---------|----------|---------------------|
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `runner_id` | integer | yes | The ID of a runner |
|
||||
|
||||
```
|
||||
curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners" -F "runner_id=9"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"active": true,
|
||||
"description": "test-2016-02-01",
|
||||
"id": 9,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
}
|
||||
```
|
||||
|
||||
## Disable a runner from project
|
||||
|
||||
Disable a specific runner from the project. It works only if the project isn't
|
||||
the only project associated with the specified runner. If so, an error is
|
||||
returned. Use the [Remove a runner](#remove-a-runner) call instead.
|
||||
|
||||
```
|
||||
DELETE /projects/:id/runners/:runner_id
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-------------|---------|----------|---------------------|
|
||||
| `id` | integer | yes | The ID of a project |
|
||||
| `runner_id` | integer | yes | The ID of a runner |
|
||||
|
||||
```
|
||||
curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners/9"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"active": true,
|
||||
"description": "test-2016-02-01",
|
||||
"id": 9,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
}
|
||||
```
|
|
@ -1,5 +1,9 @@
|
|||
# Runners API
|
||||
|
||||
_**Note:** This API is intended to be used only by Runners as their own
|
||||
communication channel. For the consumer API see the
|
||||
[new Runners API](../../api/runners.md)._
|
||||
|
||||
## Runners
|
||||
|
||||
### Retrieve all runners
|
||||
|
|
|
@ -56,5 +56,6 @@ module API
|
|||
mount Triggers
|
||||
mount Builds
|
||||
mount Variables
|
||||
mount Runners
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,7 +49,7 @@ module API
|
|||
expose :enable_ssl_verification
|
||||
end
|
||||
|
||||
class ForkedFromProject < Grape::Entity
|
||||
class BasicProjectDetails < Grape::Entity
|
||||
expose :id
|
||||
expose :name, :name_with_namespace
|
||||
expose :path, :path_with_namespace
|
||||
|
@ -67,7 +67,7 @@ module API
|
|||
expose :shared_runners_enabled
|
||||
expose :creator_id
|
||||
expose :namespace
|
||||
expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ |project, options| project.forked? }
|
||||
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
|
||||
expose :avatar_url
|
||||
expose :star_count, :forks_count
|
||||
expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? }
|
||||
|
@ -377,6 +377,20 @@ module API
|
|||
expose :name
|
||||
end
|
||||
|
||||
class RunnerDetails < Runner
|
||||
expose :tag_list
|
||||
expose :version, :revision, :platform, :architecture
|
||||
expose :contacted_at
|
||||
expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? }
|
||||
expose :projects, with: Entities::BasicProjectDetails do |runner, options|
|
||||
if options[:current_user].is_admin?
|
||||
runner.projects
|
||||
else
|
||||
options[:current_user].authorized_projects.where(id: runner.projects)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Build < Grape::Entity
|
||||
expose :id, :status, :stage, :name, :ref, :tag, :coverage
|
||||
expose :created_at, :started_at, :finished_at
|
||||
|
|
175
lib/api/runners.rb
Normal file
175
lib/api/runners.rb
Normal file
|
@ -0,0 +1,175 @@
|
|||
module API
|
||||
# Runners API
|
||||
class Runners < Grape::API
|
||||
before { authenticate! }
|
||||
|
||||
resource :runners do
|
||||
# Get runners available for user
|
||||
#
|
||||
# Example Request:
|
||||
# GET /runners
|
||||
get do
|
||||
runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared'])
|
||||
present paginate(runners), with: Entities::Runner
|
||||
end
|
||||
|
||||
# Get all runners - shared and specific
|
||||
#
|
||||
# Example Request:
|
||||
# GET /runners/all
|
||||
get 'all' do
|
||||
authenticated_as_admin!
|
||||
runners = filter_runners(Ci::Runner.all, params[:scope])
|
||||
present paginate(runners), with: Entities::Runner
|
||||
end
|
||||
|
||||
# Get runner's details
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of ther runner
|
||||
# Example Request:
|
||||
# GET /runners/:id
|
||||
get ':id' do
|
||||
runner = get_runner(params[:id])
|
||||
authenticate_show_runner!(runner)
|
||||
|
||||
present runner, with: Entities::RunnerDetails, current_user: current_user
|
||||
end
|
||||
|
||||
# Update runner's details
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of ther runner
|
||||
# description (optional) - Runner's description
|
||||
# active (optional) - Runner's status
|
||||
# tag_list (optional) - Array of tags for runner
|
||||
# Example Request:
|
||||
# PUT /runners/:id
|
||||
put ':id' do
|
||||
runner = get_runner(params[:id])
|
||||
authenticate_update_runner!(runner)
|
||||
|
||||
attrs = attributes_for_keys [:description, :active, :tag_list]
|
||||
if runner.update(attrs)
|
||||
present runner, with: Entities::RunnerDetails, current_user: current_user
|
||||
else
|
||||
render_validation_error!(runner)
|
||||
end
|
||||
end
|
||||
|
||||
# Remove runner
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of ther runner
|
||||
# Example Request:
|
||||
# DELETE /runners/:id
|
||||
delete ':id' do
|
||||
runner = get_runner(params[:id])
|
||||
authenticate_delete_runner!(runner)
|
||||
runner.destroy!
|
||||
|
||||
present runner, with: Entities::Runner
|
||||
end
|
||||
end
|
||||
|
||||
resource :projects do
|
||||
before { authorize_admin_project }
|
||||
|
||||
# Get runners available for project
|
||||
#
|
||||
# Example Request:
|
||||
# GET /projects/:id/runners
|
||||
get ':id/runners' do
|
||||
runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope])
|
||||
present paginate(runners), with: Entities::Runner
|
||||
end
|
||||
|
||||
# Enable runner for project
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of the project
|
||||
# runner_id (required) - The ID of the runner
|
||||
# Example Request:
|
||||
# POST /projects/:id/runners/:runner_id
|
||||
post ':id/runners' do
|
||||
required_attributes! [:runner_id]
|
||||
|
||||
runner = get_runner(params[:runner_id])
|
||||
authenticate_enable_runner!(runner)
|
||||
Ci::RunnerProject.create(runner: runner, project: user_project)
|
||||
|
||||
present runner, with: Entities::Runner
|
||||
end
|
||||
|
||||
# Disable project's runner
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of the project
|
||||
# runner_id (required) - The ID of the runner
|
||||
# Example Request:
|
||||
# DELETE /projects/:id/runners/:runner_id
|
||||
delete ':id/runners/:runner_id' do
|
||||
runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
|
||||
not_found!('Runner') unless runner_project
|
||||
|
||||
runner = runner_project.runner
|
||||
forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
|
||||
|
||||
runner_project.destroy
|
||||
|
||||
present runner, with: Entities::Runner
|
||||
end
|
||||
end
|
||||
|
||||
helpers do
|
||||
def filter_runners(runners, scope, options = {})
|
||||
return runners unless scope.present?
|
||||
|
||||
available_scopes = ::Ci::Runner::AVAILABLE_SCOPES
|
||||
if options[:without]
|
||||
available_scopes = available_scopes - options[:without]
|
||||
end
|
||||
|
||||
if (available_scopes & [scope]).empty?
|
||||
render_api_error!('Scope contains invalid value', 400)
|
||||
end
|
||||
|
||||
runners.send(scope)
|
||||
end
|
||||
|
||||
def get_runner(id)
|
||||
runner = Ci::Runner.find(id)
|
||||
not_found!('Runner') unless runner
|
||||
runner
|
||||
end
|
||||
|
||||
def authenticate_show_runner!(runner)
|
||||
return if runner.is_shared || current_user.is_admin?
|
||||
forbidden!("No access granted") unless user_can_access_runner?(runner)
|
||||
end
|
||||
|
||||
def authenticate_update_runner!(runner)
|
||||
return if current_user.is_admin?
|
||||
forbidden!("Runner is shared") if runner.is_shared?
|
||||
forbidden!("No access granted") unless user_can_access_runner?(runner)
|
||||
end
|
||||
|
||||
def authenticate_delete_runner!(runner)
|
||||
return if current_user.is_admin?
|
||||
forbidden!("Runner is shared") if runner.is_shared?
|
||||
forbidden!("Runner associated with more than one project") if runner.projects.count > 1
|
||||
forbidden!("No access granted") unless user_can_access_runner?(runner)
|
||||
end
|
||||
|
||||
def authenticate_enable_runner!(runner)
|
||||
forbidden!("Runner is shared") if runner.is_shared?
|
||||
return if current_user.is_admin?
|
||||
forbidden!("No access granted") unless user_can_access_runner?(runner)
|
||||
end
|
||||
|
||||
def user_can_access_runner?(runner)
|
||||
current_user.ci_authorized_runners.exists?(runner.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -25,14 +25,12 @@ FactoryGirl.define do
|
|||
"My runner#{n}"
|
||||
end
|
||||
|
||||
platform "darwin"
|
||||
platform "darwin"
|
||||
is_shared false
|
||||
active true
|
||||
|
||||
factory :ci_shared_runner do
|
||||
trait :shared do
|
||||
is_shared true
|
||||
end
|
||||
|
||||
factory :ci_specific_runner do
|
||||
is_shared false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,10 +17,10 @@ describe "Runners" do
|
|||
@project3 = FactoryGirl.create :empty_project
|
||||
@project3.team << [user, :developer]
|
||||
|
||||
@shared_runner = FactoryGirl.create :ci_shared_runner
|
||||
@specific_runner = FactoryGirl.create :ci_specific_runner
|
||||
@specific_runner2 = FactoryGirl.create :ci_specific_runner
|
||||
@specific_runner3 = FactoryGirl.create :ci_specific_runner
|
||||
@shared_runner = FactoryGirl.create :ci_runner, :shared
|
||||
@specific_runner = FactoryGirl.create :ci_runner
|
||||
@specific_runner2 = FactoryGirl.create :ci_runner
|
||||
@specific_runner3 = FactoryGirl.create :ci_runner
|
||||
@project.runners << @specific_runner
|
||||
@project2.runners << @specific_runner2
|
||||
@project3.runners << @specific_runner3
|
||||
|
@ -84,7 +84,7 @@ describe "Runners" do
|
|||
before do
|
||||
@project = FactoryGirl.create :empty_project
|
||||
@project.team << [user, :master]
|
||||
@specific_runner = FactoryGirl.create :ci_specific_runner
|
||||
@specific_runner = FactoryGirl.create :ci_runner
|
||||
@project.runners << @specific_runner
|
||||
end
|
||||
|
||||
|
|
|
@ -243,7 +243,7 @@ describe Ci::Build, models: true do
|
|||
end
|
||||
|
||||
describe :can_be_served? do
|
||||
let(:runner) { FactoryGirl.create :ci_specific_runner }
|
||||
let(:runner) { FactoryGirl.create :ci_runner }
|
||||
|
||||
before { build.project.runners << runner }
|
||||
|
||||
|
@ -285,7 +285,7 @@ describe Ci::Build, models: true do
|
|||
end
|
||||
|
||||
context 'if there are runner' do
|
||||
let(:runner) { FactoryGirl.create :ci_specific_runner }
|
||||
let(:runner) { FactoryGirl.create :ci_runner }
|
||||
|
||||
before do
|
||||
build.project.runners << runner
|
||||
|
@ -322,7 +322,7 @@ describe Ci::Build, models: true do
|
|||
it { is_expected.to be_truthy }
|
||||
|
||||
context "and there are specific runner" do
|
||||
let(:runner) { FactoryGirl.create :ci_specific_runner, contacted_at: 1.second.ago }
|
||||
let(:runner) { FactoryGirl.create :ci_runner, contacted_at: 1.second.ago }
|
||||
|
||||
before do
|
||||
build.project.runners << runner
|
||||
|
|
|
@ -39,7 +39,7 @@ describe Ci::Runner, models: true do
|
|||
|
||||
describe :assign_to do
|
||||
let!(:project) { FactoryGirl.create :empty_project }
|
||||
let!(:shared_runner) { FactoryGirl.create(:ci_shared_runner) }
|
||||
let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) }
|
||||
|
||||
before { shared_runner.assign_to(project) }
|
||||
|
||||
|
@ -52,15 +52,15 @@ describe Ci::Runner, models: true do
|
|||
subject { Ci::Runner.online }
|
||||
|
||||
before do
|
||||
@runner1 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.year.ago)
|
||||
@runner2 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago)
|
||||
@runner1 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.year.ago)
|
||||
@runner2 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago)
|
||||
end
|
||||
|
||||
it { is_expected.to eq([@runner2])}
|
||||
end
|
||||
|
||||
describe :online? do
|
||||
let(:runner) { FactoryGirl.create(:ci_shared_runner) }
|
||||
let(:runner) { FactoryGirl.create(:ci_runner, :shared) }
|
||||
|
||||
subject { runner.online? }
|
||||
|
||||
|
@ -84,7 +84,7 @@ describe Ci::Runner, models: true do
|
|||
end
|
||||
|
||||
describe :status do
|
||||
let(:runner) { FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago) }
|
||||
let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) }
|
||||
|
||||
subject { runner.status }
|
||||
|
||||
|
@ -115,7 +115,7 @@ describe Ci::Runner, models: true do
|
|||
|
||||
describe "belongs_to_one_project?" do
|
||||
it "returns false if there are two projects runner assigned to" do
|
||||
runner = FactoryGirl.create(:ci_specific_runner)
|
||||
runner = FactoryGirl.create(:ci_runner)
|
||||
project = FactoryGirl.create(:empty_project)
|
||||
project1 = FactoryGirl.create(:empty_project)
|
||||
project.runners << runner
|
||||
|
@ -125,7 +125,7 @@ describe Ci::Runner, models: true do
|
|||
end
|
||||
|
||||
it "returns true" do
|
||||
runner = FactoryGirl.create(:ci_specific_runner)
|
||||
runner = FactoryGirl.create(:ci_runner)
|
||||
project = FactoryGirl.create(:empty_project)
|
||||
project.runners << runner
|
||||
|
||||
|
|
|
@ -519,8 +519,8 @@ describe Project, models: true do
|
|||
|
||||
describe :any_runners do
|
||||
let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) }
|
||||
let(:specific_runner) { create(:ci_specific_runner) }
|
||||
let(:shared_runner) { create(:ci_shared_runner) }
|
||||
let(:specific_runner) { create(:ci_runner) }
|
||||
let(:shared_runner) { create(:ci_runner, :shared) }
|
||||
|
||||
context 'for shared runners disabled' do
|
||||
let(:shared_runners_enabled) { false }
|
||||
|
|
464
spec/requests/api/runners_spec.rb
Normal file
464
spec/requests/api/runners_spec.rb
Normal file
|
@ -0,0 +1,464 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe API::Runners, api: true do
|
||||
include ApiHelpers
|
||||
|
||||
let(:admin) { create(:user, :admin) }
|
||||
let(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
|
||||
let(:project) { create(:project, creator_id: user.id) }
|
||||
let(:project2) { create(:project, creator_id: user.id) }
|
||||
|
||||
let!(:shared_runner) { create(:ci_runner, :shared) }
|
||||
let!(:unused_specific_runner) { create(:ci_runner) }
|
||||
|
||||
let!(:specific_runner) do
|
||||
create(:ci_runner).tap do |runner|
|
||||
create(:ci_runner_project, runner: runner, project: project)
|
||||
end
|
||||
end
|
||||
|
||||
let!(:two_projects_runner) do
|
||||
create(:ci_runner).tap do |runner|
|
||||
create(:ci_runner_project, runner: runner, project: project)
|
||||
create(:ci_runner_project, runner: runner, project: project2)
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
# Set project access for users
|
||||
create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER)
|
||||
create(:project_member, user: user, project: project2, access_level: ProjectMember::MASTER)
|
||||
create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER)
|
||||
end
|
||||
|
||||
describe 'GET /runners' do
|
||||
context 'authorized user' do
|
||||
it 'should return user available runners' do
|
||||
get api('/runners', user)
|
||||
shared = json_response.any?{ |r| r['is_shared'] }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response).to be_an Array
|
||||
expect(shared).to be_falsey
|
||||
end
|
||||
|
||||
it 'should filter runners by scope' do
|
||||
get api('/runners?scope=active', user)
|
||||
shared = json_response.any?{ |r| r['is_shared'] }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response).to be_an Array
|
||||
expect(shared).to be_falsey
|
||||
end
|
||||
|
||||
it 'should avoid filtering if scope is invalid' do
|
||||
get api('/runners?scope=unknown', user)
|
||||
expect(response.status).to eq(400)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'should not return runners' do
|
||||
get api('/runners')
|
||||
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /runners/all' do
|
||||
context 'authorized user' do
|
||||
context 'with admin privileges' do
|
||||
it 'should return all runners' do
|
||||
get api('/runners/all', admin)
|
||||
shared = json_response.any?{ |r| r['is_shared'] }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response).to be_an Array
|
||||
expect(shared).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'without admin privileges' do
|
||||
it 'should not return runners list' do
|
||||
get api('/runners/all', user)
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
it 'should filter runners by scope' do
|
||||
get api('/runners/all?scope=specific', admin)
|
||||
shared = json_response.any?{ |r| r['is_shared'] }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response).to be_an Array
|
||||
expect(shared).to be_falsey
|
||||
end
|
||||
|
||||
it 'should avoid filtering if scope is invalid' do
|
||||
get api('/runners?scope=unknown', admin)
|
||||
expect(response.status).to eq(400)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'should not return runners' do
|
||||
get api('/runners')
|
||||
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /runners/:id' do
|
||||
context 'admin user' do
|
||||
context 'when runner is shared' do
|
||||
it "should return runner's details" do
|
||||
get api("/runners/#{shared_runner.id}", admin)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response['description']).to eq(shared_runner.description)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner is not shared' do
|
||||
it "should return runner's details" do
|
||||
get api("/runners/#{specific_runner.id}", admin)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response['description']).to eq(specific_runner.description)
|
||||
end
|
||||
end
|
||||
|
||||
it 'should return 404 if runner does not exists' do
|
||||
get api('/runners/9999', admin)
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
|
||||
context "runner project's administrative user" do
|
||||
context 'when runner is not shared' do
|
||||
it "should return runner's details" do
|
||||
get api("/runners/#{specific_runner.id}", user)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response['description']).to eq(specific_runner.description)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner is shared' do
|
||||
it "should return runner's details" do
|
||||
get api("/runners/#{shared_runner.id}", user)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response['description']).to eq(shared_runner.description)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'other authorized user' do
|
||||
it "should not return runner's details" do
|
||||
get api("/runners/#{specific_runner.id}", user2)
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it "should not return runner's details" do
|
||||
get api("/runners/#{specific_runner.id}")
|
||||
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /runners/:id' do
|
||||
context 'admin user' do
|
||||
context 'when runner is shared' do
|
||||
it 'should update runner' do
|
||||
description = shared_runner.description
|
||||
active = shared_runner.active
|
||||
|
||||
put api("/runners/#{shared_runner.id}", admin), description: "#{description}_updated", active: !active,
|
||||
tag_list: ['ruby2.1', 'pgsql', 'mysql']
|
||||
shared_runner.reload
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(shared_runner.description).to eq("#{description}_updated")
|
||||
expect(shared_runner.active).to eq(!active)
|
||||
expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner is not shared' do
|
||||
it 'should update runner' do
|
||||
description = specific_runner.description
|
||||
put api("/runners/#{specific_runner.id}", admin), description: 'test'
|
||||
specific_runner.reload
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(specific_runner.description).to eq('test')
|
||||
expect(specific_runner.description).not_to eq(description)
|
||||
end
|
||||
end
|
||||
|
||||
it 'should return 404 if runner does not exists' do
|
||||
put api('/runners/9999', admin), description: 'test'
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user' do
|
||||
context 'when runner is shared' do
|
||||
it 'should not update runner' do
|
||||
put api("/runners/#{shared_runner.id}", user)
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner is not shared' do
|
||||
it 'should not update runner without access to it' do
|
||||
put api("/runners/#{specific_runner.id}", user2)
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it 'should update runner with access to it' do
|
||||
description = specific_runner.description
|
||||
put api("/runners/#{specific_runner.id}", admin), description: 'test'
|
||||
specific_runner.reload
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(specific_runner.description).to eq('test')
|
||||
expect(specific_runner.description).not_to eq(description)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'should not delete runner' do
|
||||
put api("/runners/#{specific_runner.id}")
|
||||
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /runners/:id' do
|
||||
context 'admin user' do
|
||||
context 'when runner is shared' do
|
||||
it 'should delete runner' do
|
||||
expect do
|
||||
delete api("/runners/#{shared_runner.id}", admin)
|
||||
end.to change{ Ci::Runner.shared.count }.by(-1)
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner is not shared' do
|
||||
it 'should delete unused runner' do
|
||||
expect do
|
||||
delete api("/runners/#{unused_specific_runner.id}", admin)
|
||||
end.to change{ Ci::Runner.specific.count }.by(-1)
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it 'should delete used runner' do
|
||||
expect do
|
||||
delete api("/runners/#{specific_runner.id}", admin)
|
||||
end.to change{ Ci::Runner.specific.count }.by(-1)
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
it 'should return 404 if runner does not exists' do
|
||||
delete api('/runners/9999', admin)
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user' do
|
||||
context 'when runner is shared' do
|
||||
it 'should not delete runner' do
|
||||
delete api("/runners/#{shared_runner.id}", user)
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner is not shared' do
|
||||
it 'should not delete runner without access to it' do
|
||||
delete api("/runners/#{specific_runner.id}", user2)
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it 'should not delete runner with more than one associated project' do
|
||||
delete api("/runners/#{two_projects_runner.id}", user)
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it 'should delete runner for one owned project' do
|
||||
expect do
|
||||
delete api("/runners/#{specific_runner.id}", user)
|
||||
end.to change{ Ci::Runner.specific.count }.by(-1)
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'should not delete runner' do
|
||||
delete api("/runners/#{specific_runner.id}")
|
||||
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/runners' do
|
||||
context 'authorized user with master privileges' do
|
||||
it "should return project's runners" do
|
||||
get api("/projects/#{project.id}/runners", user)
|
||||
shared = json_response.any?{ |r| r['is_shared'] }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response).to be_an Array
|
||||
expect(shared).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user without master privileges' do
|
||||
it "should not return project's runners" do
|
||||
get api("/projects/#{project.id}/runners", user2)
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it "should not return project's runners" do
|
||||
get api("/projects/#{project.id}/runners")
|
||||
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /projects/:id/runners' do
|
||||
context 'authorized user' do
|
||||
it 'should enable specific runner' do
|
||||
specific_runner2 = create(:ci_runner).tap do |runner|
|
||||
create(:ci_runner_project, runner: runner, project: project2)
|
||||
end
|
||||
|
||||
expect do
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
|
||||
end.to change{ project.runners.count }.by(+1)
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it 'should avoid changes when enabling already enabled runner' do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
|
||||
end.to change{ project.runners.count }.by(0)
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it 'should not enable shared runner' do
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: shared_runner.id
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
context 'user is admin' do
|
||||
it 'should enable any specific runner' do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id
|
||||
end.to change{ project.runners.count }.by(+1)
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
end
|
||||
|
||||
context 'user is not admin' do
|
||||
it 'should not enable runner without access to' do
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: unused_specific_runner.id
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
it 'should raise an error when no runner_id param is provided' do
|
||||
post api("/projects/#{project.id}/runners", admin)
|
||||
|
||||
expect(response.status).to eq(400)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user without permissions' do
|
||||
it 'should not enable runner' do
|
||||
post api("/projects/#{project.id}/runners", user2)
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'should not enable runner' do
|
||||
post api("/projects/#{project.id}/runners")
|
||||
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /projects/:id/runners/:runner_id' do
|
||||
context 'authorized user' do
|
||||
context 'when runner have more than one associated projects' do
|
||||
it "should disable project's runner" do
|
||||
expect do
|
||||
delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
|
||||
end.to change{ project.runners.count }.by(-1)
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner have one associated projects' do
|
||||
it "should not disable project's runner" do
|
||||
expect do
|
||||
delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
|
||||
end.to change{ project.runners.count }.by(0)
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
it 'should return 404 is runner is not found' do
|
||||
delete api("/projects/#{project.id}/runners/9999", user)
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authorized user without permissions' do
|
||||
it "should not disable project's runner" do
|
||||
delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it "should not disable project's runner" do
|
||||
delete api("/projects/#{project.id}/runners/#{specific_runner.id}")
|
||||
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue