diff --git a/doc/api/admin/data_management.md b/doc/api/admin/data_management.md index 11d300f3fc5e54e0fb2b2c580869e40602242a87..91f0569043cc748d568a21371b58c385c8b02835 100644 --- a/doc/api/admin/data_management.md +++ b/doc/api/admin/data_management.md @@ -118,6 +118,30 @@ Example response: ] ``` +## Recalculate the checksum of all model records + +```plaintext +PUT /admin/data_management/:model_name/checksum +``` + +| Attribute | Type | Required | Description | +|---------------------|-------------------|----------|---------------------------------------------------------------------------------------------| +| `model_name` | string | Yes | The name of the requested model. Must belong to the `:model_name` list above. | + +This endpoint marks all records from the model for checksum recalculation. It enqueues a background job to do so. If successful, returns [`200`](../rest/troubleshooting.md#status-codes) and a JSON response containing the following information: + +| Attribute | Type | Description | +|-----------|--------|---------------------------------------------------| +| `message` | string | A information message about the success or error. | +| `status` | string | Can be "success" or "error". | + +```json +{ + "status": "success", + "message": "Batch update job has been successfully enqueued." +} +``` + ## Get information about a specific model record ```plaintext diff --git a/ee/lib/api/admin/data_management.rb b/ee/lib/api/admin/data_management.rb index 0053a57e05fb2e54ca3696c6c167e30ce1d574fc..eb8485dc23776378da370423257f171f79c55805 100644 --- a/ee/lib/api/admin/data_management.rb +++ b/ee/lib/api/admin/data_management.rb @@ -56,54 +56,19 @@ def find_models_from_record_identifier_array(identifier_array, relation) rescue ArgumentError, TypeError => e bad_request!(e) end + + def find_verifiable_model_class + model_class = Gitlab::Geo::ModelMapper.find_from_name(params[:model_name]) + not_found!(params[:model_name]) unless model_class + bad_request!("#{model_class} is not a verifiable model.") unless verifiable?(model_class) + + model_class + end end resource :admin do resource :data_management do - route_param :model_name, type: String, desc: 'The name of the model being managed' do - # Example request: - # GET /admin/data_management/:model_name - desc 'Get a list of model data' do - summary 'Retrieve all records of the requested model' - detail 'This feature is experimental.' - success code: 200, model: Entities::Admin::Model - failure [ - { code: 400, message: '400 Bad request' }, - { code: 401, message: '401 Unauthorized' }, - { code: 403, message: '403 Forbidden' }, - { code: 404, message: '404 Model Not Found' } - ] - is_array true - tags %w[data_management] - end - params do - use :pagination - requires :model_name, type: String, values: AVAILABLE_MODEL_NAMES - optional :identifiers, types: [Array[Integer], Array[String]], desc: 'The record identifiers to filter by' - optional :checksum_state, - type: String, - desc: 'The checksum status of the records to filter by', - values: VERIFICATION_STATES - end - get do - model_class = Gitlab::Geo::ModelMapper.find_from_name(params[:model_name]) - not_found!(params[:model_name]) unless model_class - - relation = model_class.respond_to?(:with_state_details) ? model_class.with_state_details : model_class - if params[:identifiers]&.compact.present? - relation = find_models_from_record_identifier_array(params[:identifiers], relation) - end - - if params[:checksum_state].present? - bad_request!("#{model_class} is not a verifiable model.") unless verifiable?(model_class) - relation = relation.with_verification_state("verification_#{params[:checksum_state]}") - end - - relation = relation.order_by_primary_key - - present paginate(relation.all, without_count: true), with: Entities::Admin::Model - end - + route_param :model_name, type: String, desc: 'The name of the model being requested' do route_param :record_identifier, types: [Integer, String], desc: 'The identifier of the model being requested' do @@ -158,10 +123,7 @@ def find_models_from_record_identifier_array(identifier_array, relation) put 'checksum' do bad_request!('Endpoint only available on primary site.') unless ::Gitlab::Geo.primary? - model_class = Gitlab::Geo::ModelMapper.find_from_name(params[:model_name]) - not_found!(params[:model_name]) unless model_class - bad_request!("#{model_class} is not a verifiable model.") unless verifiable?(model_class) - + model_class = find_verifiable_model_class model = find_model_from_record_identifier(params[:record_identifier], model_class) not_found!(params[:record_identifier]) unless model @@ -171,6 +133,80 @@ def find_models_from_record_identifier_array(identifier_array, relation) present model, with: Entities::Admin::Model end end + + # Example request: + # GET /admin/data_management/:model_name + desc 'Get a list of model data' do + summary 'Retrieve all records of the requested model' + detail 'This feature is experimental.' + success code: 200, model: Entities::Admin::Model + failure [ + { code: 400, message: '400 Bad request' }, + { code: 401, message: '401 Unauthorized' }, + { code: 403, message: '403 Forbidden' }, + { code: 404, message: '404 Model Not Found' } + ] + is_array true + tags %w[data_management] + end + params do + use :pagination + requires :model_name, type: String, values: AVAILABLE_MODEL_NAMES + optional :identifiers, types: [Array[Integer], Array[String]], desc: 'The record identifiers to filter by' + optional :checksum_state, + type: String, + desc: 'The checksum status of the records to filter by', + values: VERIFICATION_STATES + end + get do + model_class = Gitlab::Geo::ModelMapper.find_from_name(params[:model_name]) + not_found!(params[:model_name]) unless model_class + + relation = model_class.respond_to?(:with_state_details) ? model_class.with_state_details : model_class + if params[:identifiers]&.compact.present? + relation = find_models_from_record_identifier_array(params[:identifiers], relation) + end + + if params[:checksum_state].present? + bad_request!("#{model_class} is not a verifiable model.") unless verifiable?(model_class) + relation = relation.with_verification_state("verification_#{params[:checksum_state]}") + end + + relation = relation.order_by_primary_key + + present paginate(relation.all, without_count: true), with: Entities::Admin::Model + end + + # Example request: + # PUT /admin/data_management/:model_name/checksum + desc 'Recalculate the checksum of a all records for a model' do + summary 'Marks all records from a given model for checksum recalculation' + detail 'This feature is experimental.' + success code: 200, model: Entities::Admin::Model + failure [ + { code: 400, message: '400 Bad request' }, + { code: 401, message: '401 Unauthorized' }, + { code: 403, message: '403 Forbidden' }, + { code: 404, message: '404 Model Not Found' } + ] + tags %w[data_management] + end + params do + requires :model_name, type: String, values: AVAILABLE_MODEL_NAMES + end + put 'checksum' do + bad_request!('Endpoint only available on primary site.') unless ::Gitlab::Geo.primary? + find_verifiable_model_class + + service_result = ::Geo::BulkPrimaryVerificationService.new(params[:model_name]).async_execute + result = if service_result.success? + { status: 'success', message: service_result.message } + else + { status: 'error', message: service_result.message } + end + + present result + end end end end diff --git a/ee/spec/requests/api/admin/data_management_spec.rb b/ee/spec/requests/api/admin/data_management_spec.rb index 272a2d1ba273164524a154a84ee8b5b3a3906a5e..a67030562b2a01e3781d19acd62b07aa42af4782 100644 --- a/ee/spec/requests/api/admin/data_management_spec.rb +++ b/ee/spec/requests/api/admin/data_management_spec.rb @@ -375,6 +375,121 @@ def create_record_for_given_state(state) end end + describe 'PUT /admin/data_management/:model_name/checksum' do + context 'with feature flag enabled' do + let_it_be(:node) { create(:geo_node) } + let_it_be(:api_path) { "/admin/data_management/merge_request_diff/checksum" } + + before do + stub_current_geo_node(node) + stub_primary_site + end + + context 'when authenticated as admin' do + context 'when not on primary site' do + before do + allow(Gitlab::Geo).to receive(:primary?).and_return(false) + end + + it 'returns 400 bad request' do + put api(api_path, admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('400 Bad request - Endpoint only available on primary site.') + end + end + + context 'with valid model name' do + it 'returns service result' do + expect(::Geo::BulkPrimaryVerificationService).to receive(:new).with('merge_request_diff').and_call_original + + put api(api_path, admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include('status' => 'success') + end + + context 'when service returns an error' do + before do + allow(::Geo::BulkPrimaryVerificationService).to receive_message_chain(:new, :async_execute) + .and_return(ServiceResponse.error(message: 'Error')) + end + + it 'returns error message' do + put api(api_path, admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include('status' => 'error') + end + end + end + + context 'with invalid model names' do + # Edge cases - invalid inputs + it 'returns 400 for non-existent model name' do + put api('/admin/data_management/non_existent_model/checksum', admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns 404 for empty model name' do + put api('/admin/data_management/checksum', admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with URL encoding' do + # Edge cases - URL encoded characters + it 'handles URL encoded model names' do + put api('/admin/data_management/lfs%5Fobject/checksum', admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'handles URL encoded special characters' do + put api('/admin/data_management/lfs%40object/checksum', admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + + context 'when not authenticated as admin' do + # Security boundary tests + it 'denies access for regular users' do + put api(api_path, user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'denies access for unauthenticated requests' do + put api(api_path) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'denies access for admin without admin mode' do + put api(api_path, admin) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'with feature flag disabled' do + before do + Feature.disable(:geo_primary_verification_view) + end + + it 'returns 404' do + put api("/admin/data_management/terraform_state_version/checksum", admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + describe 'GET /admin/data_management/:model_name/:record_identifier' do context 'with feature flag enabled' do context 'with valid model name' do