diff --git a/app/models/project_repository_storage_move.rb b/app/models/project_repository_storage_move.rb index 8b2ddb22a487690bc4d1edb6d25e867735deaad6..e88cc5cfca628f6549a4437a6d3455d506c743c7 100644 --- a/app/models/project_repository_storage_move.rb +++ b/app/models/project_repository_storage_move.rb @@ -52,4 +52,7 @@ class ProjectRepositoryStorageMove < ApplicationRecord state :finished, value: 4 state :failed, value: 5 end + + scope :order_created_at_desc, -> { order(created_at: :desc) } + scope :with_projects, -> { includes(project: :route) } end diff --git a/changelogs/unreleased/storage_move_api.yml b/changelogs/unreleased/storage_move_api.yml new file mode 100644 index 0000000000000000000000000000000000000000..6147440bbc34dd8c20435d5537c5b9e0dc344e97 --- /dev/null +++ b/changelogs/unreleased/storage_move_api.yml @@ -0,0 +1,5 @@ +--- +title: Read only storage move API +merge_request: 31285 +author: +type: added diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index 4c34d23dcd637cdd068e4ef11ec4fd15d5da2efd..950c353e7efb3900277462609dc74eee9c44cf5e 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -139,6 +139,7 @@ The following API resources are available outside of project and group contexts | [Notification settings](notification_settings.md) | `/notification_settings` (also available for groups and projects) | | [Pages domains](pages_domains.md) | `/pages/domains` (also available for projects) | | [Projects](projects.md) | `/users/:id/projects` (also available for projects) | +| [Project Repository Storage Moves](project_repository_storage_moves.md) | `/project_repository_storage_moves` | | [Runners](runners.md) | `/runners` (also available for projects) | | [Search](search.md) | `/search` (also available for groups and projects) | | [Settings](settings.md) **(CORE ONLY)** | `/application/settings` | diff --git a/doc/api/project_repository_storage_moves.md b/doc/api/project_repository_storage_moves.md new file mode 100644 index 0000000000000000000000000000000000000000..d3d2f109b02646c00f57deb276e1eb9d7ddfd7af --- /dev/null +++ b/doc/api/project_repository_storage_moves.md @@ -0,0 +1,80 @@ +# Project repository storage move API + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31285) in GitLab 13.0. + +Project repository storage can be moved. To retrieve project repository storage moves using the API, you must [authenticate yourself](README.md#authentication) as an administrator. + +## Retrieve all project repository storage moves + +```text +GET /project_repository_storage_moves +``` + +By default, `GET` requests return 20 results at a time because the API results +are [paginated](README.md#pagination). + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://primary.example.com/api/v4/project_repository_storage_moves' +``` + +Example response: + +```json +[ + { + "id": 1, + "created_at": "2020-05-07T04:27:17.234Z", + "state": "scheduled", + "source_storage_name": "default", + "destination_storage_name": "storage2", + "project": { + "id": 1, + "description": null, + "name": "project1", + "name_with_namespace": "John Doe2 / project1", + "path": "project1", + "path_with_namespace": "namespace1/project1", + "created_at": "2020-05-07T04:27:17.016Z" + } +] +``` + +## Get a single project repository storage move + +```text +GET /project_repository_storage_moves/:id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project repository storage move | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://primary.example.com/api/v4/project_repository_storage_moves/1' +``` + +Example response: + +```json +{ + "id": 1, + "created_at": "2020-05-07T04:27:17.234Z", + "state": "scheduled", + "source_storage_name": "default", + "destination_storage_name": "storage2", + "project": { + "id": 1, + "description": null, + "name": "project1", + "name_with_namespace": "John Doe2 / project1", + "path": "project1", + "path_with_namespace": "namespace1/project1", + "created_at": "2020-05-07T04:27:17.016Z" +} +``` diff --git a/lib/api/api.rb b/lib/api/api.rb index fe8f101e7b5f65e84000ab97d9750eb2f21f86dd..c553630cbd3ff0703cec67ef8514a69674fc10d9 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -180,6 +180,7 @@ class API < Grape::API mount ::API::ProjectImport mount ::API::ProjectHooks mount ::API::ProjectMilestones + mount ::API::ProjectRepositoryStorageMoves mount ::API::Projects mount ::API::ProjectSnapshots mount ::API::ProjectSnippets diff --git a/lib/api/entities/project_repository_storage_move.rb b/lib/api/entities/project_repository_storage_move.rb new file mode 100644 index 0000000000000000000000000000000000000000..25643651a14a310701524af48edaa31a603a7dee --- /dev/null +++ b/lib/api/entities/project_repository_storage_move.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectRepositoryStorageMove < Grape::Entity + expose :id + expose :created_at + expose :human_state_name, as: :state + expose :source_storage_name + expose :destination_storage_name + expose :project, using: Entities::ProjectIdentity + end + end +end diff --git a/lib/api/project_repository_storage_moves.rb b/lib/api/project_repository_storage_moves.rb new file mode 100644 index 0000000000000000000000000000000000000000..1a63e984fbf467eeea0ea09c0428867807bcbfa5 --- /dev/null +++ b/lib/api/project_repository_storage_moves.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module API + class ProjectRepositoryStorageMoves < Grape::API + include PaginationParams + + before { authenticated_as_admin! } + + resource :project_repository_storage_moves do + desc 'Get a list of all project repository storage moves' do + detail 'This feature was introduced in GitLab 13.0.' + success Entities::ProjectRepositoryStorageMove + end + params do + use :pagination + end + get do + storage_moves = ProjectRepositoryStorageMove.with_projects.order_created_at_desc + + present paginate(storage_moves), with: Entities::ProjectRepositoryStorageMove, current_user: current_user + end + + desc 'Get a project repository storage move' do + detail 'This feature was introduced in GitLab 13.0.' + success Entities::ProjectRepositoryStorageMove + end + get ':id' do + storage_move = ProjectRepositoryStorageMove.find(params[:id]) + + present storage_move, with: Entities::ProjectRepositoryStorageMove, current_user: current_user + end + end + end +end diff --git a/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_move.json b/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_move.json new file mode 100644 index 0000000000000000000000000000000000000000..6f8a2ff58e5cbf8db3c9776c3e7910de152adac7 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_move.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "required": [ + "id", + "created_at", + "state", + "source_storage_name", + "destination_storage_name", + "project" + ], + "properties" : { + "id": { "type": "integer" }, + "created_at": { "type": "date" }, + "state": { "type": "string" }, + "source_storage_name": { "type": "string" }, + "destination_storage_name": { "type": "string" }, + "project": { "type": "object" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_moves.json b/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_moves.json new file mode 100644 index 0000000000000000000000000000000000000000..b2de185fbfe1904fcb8ef46871c227d560bb14dc --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_moves.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "./project_repository_storage_move.json" + } +} diff --git a/spec/lib/api/entities/project_repository_storage_move_spec.rb b/spec/lib/api/entities/project_repository_storage_move_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1c38c8231d462ad5d4e19de4cf9f8b52b645d5fb --- /dev/null +++ b/spec/lib/api/entities/project_repository_storage_move_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::ProjectRepositoryStorageMove do + describe '#as_json' do + subject { entity.as_json } + + let(:storage_move) { build(:project_repository_storage_move, :scheduled, destination_storage_name: 'test_second_storage') } + let(:entity) { described_class.new(storage_move) } + + it 'includes basic fields' do + is_expected.to include( + state: 'scheduled', + source_storage_name: 'default', + destination_storage_name: 'test_second_storage', + project: a_kind_of(Hash) + ) + end + end +end diff --git a/spec/requests/api/project_repository_storage_moves_spec.rb b/spec/requests/api/project_repository_storage_moves_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7ceea0178f33bc2f01ea085a3ae4d8785659e9a4 --- /dev/null +++ b/spec/requests/api/project_repository_storage_moves_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::ProjectRepositoryStorageMoves do + include AccessMatchersForRequest + + let(:user) { create(:admin) } + let!(:storage_move) { create(:project_repository_storage_move, :scheduled) } + + describe 'GET /project_repository_storage_moves' do + def get_project_repository_storage_moves + get api('/project_repository_storage_moves', user) + end + + it 'returns project repository storage moves' do + get_project_repository_storage_moves + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/project_repository_storage_moves') + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(storage_move.id) + expect(json_response.first['state']).to eq(storage_move.human_state_name) + end + + it 'avoids N+1 queries', :request_store do + # prevent `let` from polluting the control + get_project_repository_storage_moves + + control = ActiveRecord::QueryRecorder.new { get_project_repository_storage_moves } + + create(:project_repository_storage_move, :scheduled) + + expect { get_project_repository_storage_moves }.not_to exceed_query_limit(control) + end + + it 'returns the most recently created first' do + storage_move_oldest = create(:project_repository_storage_move, :scheduled, created_at: 2.days.ago) + storage_move_middle = create(:project_repository_storage_move, :scheduled, created_at: 1.day.ago) + + get api('/project_repository_storage_moves', user) + + json_ids = json_response.map {|storage_move| storage_move['id'] } + expect(json_ids).to eq([ + storage_move.id, + storage_move_middle.id, + storage_move_oldest.id + ]) + end + + describe 'permissions' do + it { expect { get_project_repository_storage_moves }.to be_allowed_for(:admin) } + it { expect { get_project_repository_storage_moves }.to be_denied_for(:user) } + end + end + + describe 'GET /project_repository_storage_moves/:id' do + let(:project_repository_storage_move_id) { storage_move.id } + + def get_project_repository_storage_move + get api("/project_repository_storage_moves/#{project_repository_storage_move_id}", user) + end + + it 'returns a project repository storage move' do + get_project_repository_storage_move + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/project_repository_storage_move') + expect(json_response['id']).to eq(storage_move.id) + expect(json_response['state']).to eq(storage_move.human_state_name) + end + + context 'non-existent project repository storage move' do + let(:project_repository_storage_move_id) { non_existing_record_id } + + it 'returns not found' do + get_project_repository_storage_move + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe 'permissions' do + it { expect { get_project_repository_storage_move }.to be_allowed_for(:admin) } + it { expect { get_project_repository_storage_move }.to be_denied_for(:user) } + end + end +end