summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShinya Maeda <gitlab.shinyamaeda@gmail.com>2017-05-12 04:12:04 +0900
committerShinya Maeda <gitlab.shinyamaeda@gmail.com>2017-05-30 23:55:08 +0900
commitfbd3b3d8a245072121784df11b7b41d3257b989f (patch)
tree53fd84be85bd74545d59433b1b5fcf61f5d4910e
parenta16cbab3bb371941f51c3c4178b8b807de000ca8 (diff)
downloadgitlab-ce-fbd3b3d8a245072121784df11b7b41d3257b989f.tar.gz
Add API support for pipeline schedule
-rw-r--r--app/models/ci/pipeline_schedule.rb4
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb8
-rw-r--r--lib/api/pipeline_schedules.rb127
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule.json64
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedules.json4
-rw-r--r--spec/requests/api/pipeline_schedules_spec.rb278
7 files changed, 486 insertions, 0 deletions
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 07213ca608a..58cf62513d3 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -40,6 +40,10 @@ module Ci
self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
end
+ def last_pipeline
+ self.pipelines&.last
+ end
+
def schedule_next_run!
save! # with set_next_run_at
rescue ActiveRecord::RecordInvalid
diff --git a/lib/api/api.rb b/lib/api/api.rb
index ac113c5200d..bbdd2039f43 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -110,6 +110,7 @@ module API
mount ::API::Notes
mount ::API::NotificationSettings
mount ::API::Pipelines
+ mount ::API::PipelineSchedules
mount ::API::ProjectHooks
mount ::API::Projects
mount ::API::ProjectSnippets
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 8c5e5c91769..1f1942b2ec1 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -686,6 +686,14 @@ module API
expose :coverage
end
+ class PipelineSchedule < Grape::Entity
+ expose :id
+ expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active
+ expose :created_at, :updated_at, :deleted_at
+ expose :last_pipeline, using: Entities::Pipeline, if: -> (pipeline_schedule, opts) { pipeline_schedule.last_pipeline.present? }
+ expose :owner, using: Entities::UserBasic
+ end
+
class EnvironmentBasic < Grape::Entity
expose :id, :name, :slug, :external_url
end
diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb
new file mode 100644
index 00000000000..32fa5a86fab
--- /dev/null
+++ b/lib/api/pipeline_schedules.rb
@@ -0,0 +1,127 @@
+module API
+ class PipelineSchedules < Grape::API
+ include PaginationParams
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get pipeline_schedules list' do
+ success Entities::PipelineSchedule
+ end
+ params do
+ use :pagination
+ end
+ get ':id/pipeline_schedules' do
+ authenticate!
+ authorize! :read_pipeline_schedule, user_project
+
+ pipeline_schedules = user_project.pipeline_schedules
+
+ present paginate(pipeline_schedules), with: Entities::PipelineSchedule
+ end
+
+ desc 'Get specific pipeline_schedule of a project' do
+ success Entities::PipelineSchedule
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline_schedule ID'
+ end
+ get ':id/pipeline_schedules/:pipeline_schedule_id' do
+ authenticate!
+ authorize! :read_pipeline_schedule, user_project
+
+ pipeline_schedule = user_project.pipeline_schedules.find(params.delete(:pipeline_schedule_id))
+ return not_found!('PipelineSchedule') unless pipeline_schedule
+
+ present pipeline_schedule, with: Entities::PipelineSchedule
+ end
+
+ desc 'Create a pipeline_schedule' do
+ success Entities::PipelineSchedule
+ end
+ params do
+ requires :description, type: String, desc: 'The pipeline_schedule description'
+ requires :ref, type: String, desc: 'The pipeline_schedule ref'
+ requires :cron, type: String, desc: 'The pipeline_schedule cron'
+ requires :cron_timezone, type: String, desc: 'The pipeline_schedule cron_timezone'
+ requires :active, type: Boolean, desc: 'The pipeline_schedule active'
+ end
+ post ':id/pipeline_schedules' do
+ authenticate!
+ authorize! :create_pipeline_schedule, user_project
+
+ pipeline_schedule = user_project.pipeline_schedules.create(
+ declared_params(include_missing: false).merge(owner: current_user))
+
+ if pipeline_schedule.valid?
+ present pipeline_schedule, with: Entities::PipelineSchedule
+ else
+ render_validation_error!(pipeline_schedule)
+ end
+ end
+
+ desc 'Update a pipeline_schedule' do
+ success Entities::PipelineSchedule
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline_schedule ID'
+ optional :description, type: String, desc: 'The pipeline_schedule description'
+ optional :ref, type: String, desc: 'The pipeline_schedule ref'
+ optional :cron, type: String, desc: 'The pipeline_schedule cron'
+ optional :cron_timezone, type: String, desc: 'The pipeline_schedule cron_timezone'
+ optional :active, type: Boolean, desc: 'The pipeline_schedule active'
+ end
+ put ':id/pipeline_schedules/:pipeline_schedule_id' do
+ authenticate!
+ authorize! :create_pipeline_schedule, user_project
+
+ pipeline_schedule = user_project.pipeline_schedules.find(params.delete(:pipeline_schedule_id))
+ return not_found!('PipelineSchedule') unless pipeline_schedule
+
+ if pipeline_schedule.update(declared_params(include_missing: false))
+ present pipeline_schedule, with: Entities::PipelineSchedule
+ else
+ render_validation_error!(pipeline_schedule)
+ end
+ end
+
+ desc 'Take ownership of pipeline_schedule' do
+ success Entities::PipelineSchedule
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline_schedule ID'
+ end
+ post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
+ authenticate!
+ authorize! :create_pipeline_schedule, user_project
+
+ pipeline_schedule = user_project.pipeline_schedules.find(params.delete(:pipeline_schedule_id))
+ return not_found!('PipelineSchedule') unless pipeline_schedule
+
+ if pipeline_schedule.update(owner: current_user)
+ status :ok
+ present pipeline_schedule, with: Entities::PipelineSchedule
+ else
+ render_validation_error!(pipeline_schedule)
+ end
+ end
+
+ desc 'Delete a pipeline_schedule' do
+ success Entities::PipelineSchedule
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline_schedule ID'
+ end
+ delete ':id/pipeline_schedules/:pipeline_schedule_id' do
+ authenticate!
+ authorize! :admin_pipeline_schedule, user_project
+
+ pipeline_schedule = user_project.pipeline_schedules.find(params.delete(:pipeline_schedule_id))
+ return not_found!('PipelineSchedule') unless pipeline_schedule
+
+ present pipeline_schedule.destroy, with: Entities::PipelineSchedule
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json
new file mode 100644
index 00000000000..46309b212a1
--- /dev/null
+++ b/spec/fixtures/api/schemas/pipeline_schedule.json
@@ -0,0 +1,64 @@
+{
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "description": { "type": "string" },
+ "ref": { "type": "string" },
+ "cron": { "type": "string" },
+ "cron_timezone": { "type": "string" },
+ "next_run_at": { "type": "date" },
+ "active": { "type": "boolean" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "deleted_at": { "type": "date" },
+ "last_pipeline": {
+ "type": ["object", "null"],
+ "properties": {
+ "id": { "type": "integer" },
+ "sha": { "type": "string" },
+ "ref": { "type": "string" },
+ "status": { "type": "string" },
+ "before_sha": { "type": ["string", "null"] },
+ "tag": { "type": ["boolean", "null"] },
+ "yaml_errors": { "type": ["string", "null"] },
+ "user": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "started_at": { "type": "date" },
+ "finished_at": { "type": "date" },
+ "committed_at": { "type": ["string", "null"] },
+ "duration": { "type": ["integer", "null"] },
+ "coverage": { "type": ["string", "null"] }
+ },
+ "additionalProperties": false
+ },
+ "owner": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "id", "description", "ref", "cron", "cron_timezone", "next_run_at",
+ "active", "created_at", "updated_at", "deleted_at", "owner"
+ ],
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/pipeline_schedules.json b/spec/fixtures/api/schemas/pipeline_schedules.json
new file mode 100644
index 00000000000..173a28d2505
--- /dev/null
+++ b/spec/fixtures/api/schemas/pipeline_schedules.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "pipeline_schedule.json" }
+}
diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb
new file mode 100644
index 00000000000..31a2a4d576c
--- /dev/null
+++ b/spec/requests/api/pipeline_schedules_spec.rb
@@ -0,0 +1,278 @@
+require 'spec_helper'
+
+describe API::PipelineSchedules do
+ let(:developer) { create(:user) }
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :repository) }
+
+ before do
+ project.add_developer(developer)
+ end
+
+ describe 'GET /projects/:id/pipeline_schedules' do
+ context 'authenticated user with valid permissions' do
+ before do
+ create(:ci_pipeline_schedule, project: project, owner: developer)
+ .tap do |pipeline_schedule|
+ pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
+ end
+ end
+
+ it 'returns list of pipeline_schedules' do
+ get api("/projects/#{project.id}/pipeline_schedules", developer)
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(response).to match_response_schema('pipeline_schedules')
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not return pipeline_schedules list' do
+ get api("/projects/#{project.id}/pipeline_schedules", user)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not return pipeline_schedules list' do
+ get api("/projects/#{project.id}/pipeline_schedules")
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
+ let(:pipeline_schedule) do
+ create(:ci_pipeline_schedule, project: project, owner: developer)
+ .tap do |pipeline_schedule|
+ pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
+ end
+ end
+
+ context 'authenticated user with valid permissions' do
+ it 'returns pipeline_schedule details' do
+ get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('pipeline_schedule')
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do
+ get api("/projects/#{project.id}/pipeline_schedules/-5", developer)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not return pipeline_schedules list' do
+ get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not return pipeline_schedules list' do
+ get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipeline_schedules' do
+ let(:description) { 'pipeline_schedule' }
+ let(:ref) { 'master' }
+ let(:cron) { '* * * * *' }
+ let(:cron_timezone) { 'UTC' }
+ let(:active) { true }
+
+ context 'authenticated user with valid permissions' do
+ context 'with required parameters' do
+ it 'creates pipeline_schedule' do
+ expect do
+ post api("/projects/#{project.id}/pipeline_schedules", developer),
+ description: description, ref: ref, cron: cron,
+ cron_timezone: cron_timezone, active: active
+ end
+ .to change{project.pipeline_schedules.count}.by(1)
+
+ expect(response).to have_http_status(:created)
+ expect(response).to match_response_schema('pipeline_schedule')
+ expect(json_response['description']).to eq(description)
+ expect(json_response['ref']).to eq(ref)
+ expect(json_response['cron']).to eq(cron)
+ expect(json_response['cron_timezone']).to eq(cron_timezone)
+ expect(json_response['active']).to eq(active)
+ end
+ end
+
+ context 'without required parameters' do
+ it 'does not create pipeline_schedule' do
+ post api("/projects/#{project.id}/pipeline_schedules", developer)
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not create pipeline_schedule' do
+ post api("/projects/#{project.id}/pipeline_schedules", user),
+ description: description, ref: ref, cron: cron,
+ cron_timezone: cron_timezone, active: active
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not create pipeline_schedule' do
+ post api("/projects/#{project.id}/pipeline_schedules"),
+ description: description, ref: ref, cron: cron,
+ cron_timezone: cron_timezone, active: active
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
+ let(:pipeline_schedule) do
+ create(:ci_pipeline_schedule, project: project, owner: developer)
+ end
+
+ context 'authenticated user with valid permissions' do
+ let(:new_ref) { 'patch-x' }
+
+ it 'updates ref' do
+ put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer),
+ ref: new_ref
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('pipeline_schedule')
+ expect(json_response['ref']).to eq(new_ref)
+ end
+
+ let(:new_cron) { '1 2 3 4 *' }
+
+ it 'updates cron' do
+ put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer),
+ cron: new_cron
+ pipeline_schedule.reload
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('pipeline_schedule')
+ expect(json_response['cron']).to eq(new_cron)
+ expect(pipeline_schedule.next_run_at.min).to eq(1)
+ expect(pipeline_schedule.next_run_at.hour).to eq(2)
+ expect(pipeline_schedule.next_run_at.day).to eq(3)
+ expect(pipeline_schedule.next_run_at.month).to eq(4)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not update pipeline_schedule' do
+ put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not update pipeline_schedule' do
+ put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
+ let(:pipeline_schedule) do
+ create(:ci_pipeline_schedule, project: project, owner: developer)
+ end
+
+ context 'authenticated user with valid permissions' do
+ let(:developer2) { create(:user) }
+
+ before do
+ project.add_developer(developer2)
+ end
+
+ it 'updates owner' do
+ post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", developer2)
+ pipeline_schedule.reload
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('pipeline_schedule')
+ expect(pipeline_schedule.owner).to eq(developer2)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not update owner' do
+ post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", user)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not update owner' do
+ post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership")
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
+ let(:master) { create(:user) }
+
+ let!(:pipeline_schedule) do
+ create(:ci_pipeline_schedule, project: project, owner: developer)
+ end
+
+ before do
+ project.add_master(master)
+ end
+
+ context 'authenticated user with valid permissions' do
+ it 'deletes pipeline_schedule' do
+ expect do
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", master)
+ end.to change{project.pipeline_schedules.count}.by(-1)
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('pipeline_schedule')
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do
+ delete api("/projects/#{project.id}/pipeline_schedules/-5", master)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not delete pipeline_schedule' do
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not delete pipeline_schedule' do
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+end