Add snippet repository storage move API endpoints

This commits adds the necessary endpoints to move
snippet repository storages.
parent f749620c
---
title: Add snippet repository storage move API endpoints
merge_request: 49228
author:
type: added
......@@ -251,6 +251,7 @@ module API
mount ::API::Services
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::SnippetRepositoryStorageMoves
mount ::API::Snippets
mount ::API::Statistics
mount ::API::Submodules
......
# frozen_string_literal: true
module API
class SnippetRepositoryStorageMoves < ::API::Base
include PaginationParams
before { authenticated_as_admin! }
feature_category :gitaly
resource :snippet_repository_storage_moves do
desc 'Get a list of all snippet repository storage moves' do
detail 'This feature was introduced in GitLab 13.8.'
success Entities::SnippetRepositoryStorageMove
end
params do
use :pagination
end
get do
storage_moves = SnippetRepositoryStorageMove.order_created_at_desc
present paginate(storage_moves), with: Entities::SnippetRepositoryStorageMove, current_user: current_user
end
desc 'Get a snippet repository storage move' do
detail 'This feature was introduced in GitLab 13.8.'
success Entities::SnippetRepositoryStorageMove
end
params do
requires :repository_storage_move_id, type: Integer, desc: 'The ID of a snippet repository storage move'
end
get ':repository_storage_move_id' do
storage_move = SnippetRepositoryStorageMove.find(params[:repository_storage_move_id])
present storage_move, with: Entities::SnippetRepositoryStorageMove, current_user: current_user
end
desc 'Schedule bulk snippet repository storage moves' do
detail 'This feature was introduced in GitLab 13.8.'
end
params do
requires :source_storage_name, type: String, desc: 'The source storage shard', values: -> { Gitlab.config.repositories.storages.keys }
optional :destination_storage_name, type: String, desc: 'The destination storage shard', values: -> { Gitlab.config.repositories.storages.keys }
end
post do
::Snippets::ScheduleBulkRepositoryShardMovesService.enqueue(
declared_params[:source_storage_name],
declared_params[:destination_storage_name]
)
accepted!
end
end
params do
requires :id, type: String, desc: 'The ID of a snippet'
end
resource :snippets do
helpers do
def user_snippet
Snippet.find_by(id: params[:id]) # rubocop: disable CodeReuse/ActiveRecord
end
end
desc 'Get a list of all snippets repository storage moves' do
detail 'This feature was introduced in GitLab 13.8.'
success Entities::SnippetRepositoryStorageMove
end
params do
use :pagination
end
get ':id/repository_storage_moves' do
storage_moves = user_snippet.repository_storage_moves.order_created_at_desc
present paginate(storage_moves), with: Entities::SnippetRepositoryStorageMove, current_user: current_user
end
desc 'Get a snippet repository storage move' do
detail 'This feature was introduced in GitLab 13.8.'
success Entities::SnippetRepositoryStorageMove
end
params do
requires :repository_storage_move_id, type: Integer, desc: 'The ID of a snippet repository storage move'
end
get ':id/repository_storage_moves/:repository_storage_move_id' do
storage_move = user_snippet.repository_storage_moves.find(params[:repository_storage_move_id])
present storage_move, with: Entities::SnippetRepositoryStorageMove, current_user: current_user
end
desc 'Schedule a snippet repository storage move' do
detail 'This feature was introduced in GitLab 13.8.'
success Entities::SnippetRepositoryStorageMove
end
params do
optional :destination_storage_name, type: String, desc: 'The destination storage shard'
end
post ':id/repository_storage_moves' do
storage_move = user_snippet.repository_storage_moves.build(
declared_params.merge(source_storage_name: user_snippet.repository_storage)
)
if storage_move.schedule
present storage_move, with: Entities::SnippetRepositoryStorageMove, current_user: current_user
else
render_validation_error!(storage_move)
end
end
end
end
end
{
"type": "object",
"required": [
"id",
"created_at",
"state",
"source_storage_name",
"destination_storage_name",
"snippet"
],
"properties" : {
"id": { "type": "integer" },
"created_at": { "type": "date" },
"state": { "type": "string" },
"source_storage_name": { "type": "string" },
"destination_storage_name": { "type": "string" },
"snippet": { "type": "object" }
},
"additionalProperties": false
}
{
"type": "array",
"items": {
"$ref": "./snippet_repository_storage_move.json"
}
}
......@@ -3,220 +3,10 @@
require 'spec_helper'
RSpec.describe API::ProjectRepositoryStorageMoves do
include AccessMatchersForRequest
let_it_be(:user) { create(:admin) }
let_it_be(:project) { create(:project, :repository).tap { |project| project.track_project_repository } }
let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: project) }
shared_examples 'get single project repository storage move' do
let(:project_repository_storage_move_id) { storage_move.id }
def get_project_repository_storage_move
get api(url, 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
shared_examples 'get project repository storage move list' do
def get_project_repository_storage_moves
get api(url, 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, container: project)
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, container: project, created_at: 2.days.ago)
storage_move_middle = create(:project_repository_storage_move, :scheduled, container: project, created_at: 1.day.ago)
get_project_repository_storage_moves
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' do
it_behaves_like 'get project repository storage move list' do
let(:url) { '/project_repository_storage_moves' }
end
end
describe 'GET /project_repository_storage_moves/:repository_storage_move_id' do
it_behaves_like 'get single project repository storage move' do
let(:url) { "/project_repository_storage_moves/#{project_repository_storage_move_id}" }
end
end
describe 'GET /projects/:id/repository_storage_moves' do
it_behaves_like 'get project repository storage move list' do
let(:url) { "/projects/#{project.id}/repository_storage_moves" }
end
end
describe 'GET /projects/:id/repository_storage_moves/:repository_storage_move_id' do
it_behaves_like 'get single project repository storage move' do
let(:url) { "/projects/#{project.id}/repository_storage_moves/#{project_repository_storage_move_id}" }
end
end
describe 'POST /projects/:id/repository_storage_moves' do
let(:url) { "/projects/#{project.id}/repository_storage_moves" }
let(:destination_storage_name) { 'test_second_storage' }
def create_project_repository_storage_move
post api(url, user), params: { destination_storage_name: destination_storage_name }
end
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
it 'schedules a project repository storage move' do
create_project_repository_storage_move
storage_move = project.repository_storage_moves.last
expect(response).to have_gitlab_http_status(:created)
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('scheduled')
expect(json_response['source_storage_name']).to eq('default')
expect(json_response['destination_storage_name']).to eq(destination_storage_name)
end
describe 'permissions' do
it { expect { create_project_repository_storage_move }.to be_allowed_for(:admin) }
it { expect { create_project_repository_storage_move }.to be_denied_for(:user) }
end
context 'destination_storage_name is missing' do
let(:destination_storage_name) { nil }
it 'schedules a project repository storage move' do
create_project_repository_storage_move
storage_move = project.repository_storage_moves.last
expect(response).to have_gitlab_http_status(:created)
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('scheduled')
expect(json_response['source_storage_name']).to eq('default')
expect(json_response['destination_storage_name']).to be_present
end
end
end
describe 'POST /project_repository_storage_moves' do
let(:source_storage_name) { 'default' }
let(:destination_storage_name) { 'test_second_storage' }
def create_project_repository_storage_moves
post api('/project_repository_storage_moves', user), params: {
source_storage_name: source_storage_name,
destination_storage_name: destination_storage_name
}
end
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
it 'schedules the worker' do
expect(ProjectScheduleBulkRepositoryShardMovesWorker).to receive(:perform_async).with(source_storage_name, destination_storage_name)
create_project_repository_storage_moves
expect(response).to have_gitlab_http_status(:accepted)
end
context 'source_storage_name is invalid' do
let(:destination_storage_name) { 'not-a-real-storage' }
it 'gives an error' do
create_project_repository_storage_moves
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'destination_storage_name is missing' do
let(:destination_storage_name) { nil }
it 'schedules the worker' do
expect(ProjectScheduleBulkRepositoryShardMovesWorker).to receive(:perform_async).with(source_storage_name, destination_storage_name)
create_project_repository_storage_moves
expect(response).to have_gitlab_http_status(:accepted)
end
end
context 'destination_storage_name is invalid' do
let(:destination_storage_name) { 'not-a-real-storage' }
it 'gives an error' do
create_project_repository_storage_moves
expect(response).to have_gitlab_http_status(:bad_request)
end
end
describe 'normal user' do
it { expect { create_project_repository_storage_moves }.to be_denied_for(:user) }
end
it_behaves_like 'repository_storage_moves API', 'projects' do
let_it_be(:container) { create(:project, :repository).tap { |project| project.track_project_repository } }
let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
let(:repository_storage_move_factory) { :project_repository_storage_move }
let(:bulk_worker_klass) { ProjectScheduleBulkRepositoryShardMovesWorker }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::SnippetRepositoryStorageMoves do
it_behaves_like 'repository_storage_moves API', 'snippets' do
let_it_be(:container) { create(:snippet, :repository).tap { |snippet| snippet.create_repository } }
let_it_be(:storage_move) { create(:snippet_repository_storage_move, :scheduled, container: container) }
let(:repository_storage_move_factory) { :snippet_repository_storage_move }
let(:bulk_worker_klass) { SnippetScheduleBulkRepositoryShardMovesWorker }
end
end
# frozen_string_literal: true
RSpec.shared_examples 'repository_storage_moves API' do |container_type|
include AccessMatchersForRequest
let_it_be(:user) { create(:admin) }
shared_examples 'get single container repository storage move' do
let(:repository_storage_move_id) { storage_move.id }
def get_container_repository_storage_move
get api(url, user)
end
it 'returns a container repository storage move', :aggregate_failures do
get_container_repository_storage_move
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_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 container repository storage move' do
let(:repository_storage_move_id) { non_existing_record_id }
it 'returns not found' do
get_container_repository_storage_move
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'permissions' do
it { expect { get_container_repository_storage_move }.to be_allowed_for(:admin) }
it { expect { get_container_repository_storage_move }.to be_denied_for(:user) }
end
end
shared_examples 'get container repository storage move list' do
def get_container_repository_storage_moves
get api(url, user)
end
it 'returns container repository storage moves', :aggregate_failures do
get_container_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/#{container_type.singularize}_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_container_repository_storage_moves
control = ActiveRecord::QueryRecorder.new { get_container_repository_storage_moves }
create(repository_storage_move_factory, :scheduled, container: container)
expect { get_container_repository_storage_moves }.not_to exceed_query_limit(control)
end
it 'returns the most recently created first' do
storage_move_oldest = create(repository_storage_move_factory, :scheduled, container: container, created_at: 2.days.ago)
storage_move_middle = create(repository_storage_move_factory, :scheduled, container: container, created_at: 1.day.ago)
get_container_repository_storage_moves
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_container_repository_storage_moves }.to be_allowed_for(:admin) }
it { expect { get_container_repository_storage_moves }.to be_denied_for(:user) }
end
end
describe "GET /#{container_type}/:id/repository_storage_moves" do
it_behaves_like 'get container repository storage move list' do
let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" }
end
end
describe "GET /#{container_type}/:id/repository_storage_moves/:repository_storage_move_id" do
it_behaves_like 'get single container repository storage move' do
let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves/#{repository_storage_move_id}" }
end
end
describe "GET /#{container_type.singularize}_repository_storage_moves" do
it_behaves_like 'get container repository storage move list' do
let(:url) { "/#{container_type.singularize}_repository_storage_moves" }
end
end
describe "GET /#{container_type.singularize}_repository_storage_moves/:repository_storage_move_id" do
it_behaves_like 'get single container repository storage move' do
let(:url) { "/#{container_type.singularize}_repository_storage_moves/#{repository_storage_move_id}" }
end
end
describe "POST /#{container_type}/:id/repository_storage_moves" do
let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" }
let(:destination_storage_name) { 'test_second_storage' }
def create_container_repository_storage_move
post api(url, user), params: { destination_storage_name: destination_storage_name }
end
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
it 'schedules a container repository storage move', :aggregate_failures do
create_container_repository_storage_move
storage_move = container.repository_storage_moves.last
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_move")
expect(json_response['id']).to eq(storage_move.id)
expect(json_response['state']).to eq('scheduled')
expect(json_response['source_storage_name']).to eq('default')
expect(json_response['destination_storage_name']).to eq(destination_storage_name)
end
describe 'permissions' do
it { expect { create_container_repository_storage_move }.to be_allowed_for(:admin) }
it { expect { create_container_repository_storage_move }.to be_denied_for(:user) }
end
context 'destination_storage_name is missing', :aggregate_failures do
let(:destination_storage_name) { nil }
it 'schedules a container repository storage move' do
create_container_repository_storage_move
storage_move = container.repository_storage_moves.last
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_move")
expect(json_response['id']).to eq(storage_move.id)
expect(json_response['state']).to eq('scheduled')
expect(json_response['source_storage_name']).to eq('default')
expect(json_response['destination_storage_name']).to be_present
end
end
end
describe "POST /#{container_type.singularize}_repository_storage_moves" do
let(:url) { "/#{container_type.singularize}_repository_storage_moves" }
let(:source_storage_name) { 'default' }
let(:destination_storage_name) { 'test_second_storage' }
def create_container_repository_storage_moves
post api(url, user), params: {
source_storage_name: source_storage_name,
destination_storage_name: destination_storage_name
}
end
before do
stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
end
it 'schedules the worker' do
expect(bulk_worker_klass).to receive(:perform_async).with(source_storage_name, destination_storage_name)
create_container_repository_storage_moves
expect(response).to have_gitlab_http_status(:accepted)
end
context 'source_storage_name is invalid' do
let(:destination_storage_name) { 'not-a-real-storage' }
it 'gives an error' do
create_container_repository_storage_moves
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'destination_storage_name is missing' do
let(:destination_storage_name) { nil }
it 'schedules the worker' do
expect(bulk_worker_klass).to receive(:perform_async).with(source_storage_name, destination_storage_name)
create_container_repository_storage_moves
expect(response).to have_gitlab_http_status(:accepted)
end
end
context 'destination_storage_name is invalid' do
let(:destination_storage_name) { 'not-a-real-storage' }
it 'gives an error' do
create_container_repository_storage_moves
expect(response).to have_gitlab_http_status(:bad_request)
end
end
describe 'normal user' do
it { expect { create_container_repository_storage_moves }.to be_denied_for(:user) }
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment