diff options
author | Shinya Maeda <gitlab.shinyamaeda@gmail.com> | 2017-05-12 04:12:04 +0900 |
---|---|---|
committer | Shinya Maeda <gitlab.shinyamaeda@gmail.com> | 2017-05-30 23:55:08 +0900 |
commit | fbd3b3d8a245072121784df11b7b41d3257b989f (patch) | |
tree | 53fd84be85bd74545d59433b1b5fcf61f5d4910e | |
parent | a16cbab3bb371941f51c3c4178b8b807de000ca8 (diff) | |
download | gitlab-ce-fbd3b3d8a245072121784df11b7b41d3257b989f.tar.gz |
Add API support for pipeline schedule
-rw-r--r-- | app/models/ci/pipeline_schedule.rb | 4 | ||||
-rw-r--r-- | lib/api/api.rb | 1 | ||||
-rw-r--r-- | lib/api/entities.rb | 8 | ||||
-rw-r--r-- | lib/api/pipeline_schedules.rb | 127 | ||||
-rw-r--r-- | spec/fixtures/api/schemas/pipeline_schedule.json | 64 | ||||
-rw-r--r-- | spec/fixtures/api/schemas/pipeline_schedules.json | 4 | ||||
-rw-r--r-- | spec/requests/api/pipeline_schedules_spec.rb | 278 |
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 |