diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md index f5e82e654621af2250351ca12c8647e501dd321b..63b08976267fe91982667107d207c0c885901511 100644 --- a/doc/api/geo_nodes.md +++ b/doc/api/geo_nodes.md @@ -69,6 +69,8 @@ Example response: Updates an existing Geo secondary node. The primary node cannot be edited. +_This can only be run against a primary Geo node._ + ``` PUT /geo_nodes/:id ``` @@ -112,6 +114,8 @@ DELETE /geo_nodes/:id To repair the OAuth authentication of a Geo node. +_This can only be run against a primary Geo node._ + ``` POST /geo_nodes/:id/repair ``` @@ -131,7 +135,7 @@ Example response: } ``` -## Retrieve status about all secondary Geo nodes +## Retrieve status about all Geo nodes ``` GET /geo_nodes/status @@ -145,6 +149,62 @@ Example response: ```json [ + { + "geo_node_id": 1, + "healthy": true, + "health": "Healthy", + "health_status": "Healthy", + "missing_oauth_application": false, + "attachments_count": 1, + "attachments_synced_count": nil, + "attachments_failed_count": nil, + "attachments_synced_missing_on_primary_count": 0, + "attachments_synced_in_percentage": "0.00%", + "db_replication_lag_seconds": nil, + "lfs_objects_count": 0, + "lfs_objects_synced_count": nil, + "lfs_objects_failed_count": nil, + "lfs_objects_synced_missing_on_primary_count": 0, + "lfs_objects_synced_in_percentage": "0.00%", + "job_artifacts_count": 2, + "job_artifacts_synced_count": nil, + "job_artifacts_failed_count": nil, + "job_artifacts_synced_missing_on_primary_count": 0, + "job_artifacts_synced_in_percentage": "0.00%", + "repositories_count": 41, + "repositories_failed_count": nil, + "repositories_synced_count": nil, + "repositories_synced_in_percentage": "0.00%", + "wikis_count": 41, + "wikis_failed_count": nil, + "wikis_synced_count": nil, + "wikis_synced_in_percentage": "0.00%", + "replication_slots_count": 1, + "replication_slots_used_count": 1, + "replication_slots_used_in_percentage": "100.00%", + "replication_slots_max_retained_wal_bytes": 0, + "repositories_checksummed_count": 20, + "repositories_checksum_failed_count": 5, + "repositories_checksummed_in_percentage": "48.78%", + "wikis_checksummed_count": 10, + "wikis_checksum_failed_count": 3, + "wikis_checksummed_in_percentage": "24.39%", + "repositories_verified_count": 20, + "repositories_verification_failed_count": 5, + "repositories_verified_in_percentage": "48.78%", + "repositories_checksum_mismatch_count": 3, + "wikis_verified_count": 10, + "wikis_verification_failed_count": 3, + "wikis_verified_in_percentage": "24.39%", + "wikis_checksum_mismatch_count": 1, + "last_event_id": 23, + "last_event_timestamp": 1509681166, + "cursor_last_event_id": nil, + "cursor_last_event_timestamp": 0, + "last_successful_status_check_timestamp": 1510125024, + "version": "10.3.0", + "revision": "33d33a096a", + }, { "geo_node_id": 2, "healthy": true, @@ -175,6 +235,10 @@ Example response: "wikis_failed_count": 0, "wikis_synced_count": 41, "wikis_synced_in_percentage": "100.00%", + "replication_slots_count": nil, + "replication_slots_used_count": nil, + "replication_slots_used_in_percentage": "0.00%", + "replication_slots_max_retained_wal_bytes": nil, "repositories_checksummed_count": 20, "repositories_checksum_failed_count": 5, "repositories_checksummed_in_percentage": "48.78%", @@ -200,7 +264,7 @@ Example response: ] ``` -## Retrieve status about a specific secondary Geo node +## Retrieve status about a specific Geo node ``` GET /geo_nodes/:id/status @@ -239,6 +303,14 @@ Example response: "repositories_failed_count": 1, "repositories_synced_count": 40, "repositories_synced_in_percentage": "97.56%", + "wikis_count": 41, + "wikis_failed_count": 0, + "wikis_synced_count": 41, + "wikis_synced_in_percentage": "100.00%", + "replication_slots_count": nil, + "replication_slots_used_count": nil, + "replication_slots_used_in_percentage": "0.00%", + "replication_slots_max_retained_wal_bytes": nil, "last_event_id": 23, "last_event_timestamp": 1509681166, "cursor_last_event_id": 23, diff --git a/ee/changelogs/unreleased/131-qa-test-geo-status-api.yml b/ee/changelogs/unreleased/131-qa-test-geo-status-api.yml new file mode 100644 index 0000000000000000000000000000000000000000..94e5c16298b835bd3c9c39b54533737a31a6d3f0 --- /dev/null +++ b/ee/changelogs/unreleased/131-qa-test-geo-status-api.yml @@ -0,0 +1,5 @@ +--- +title: Add missing fields to the API documentation for the status of Geo Nodes +merge_request: 3865 +author: +type: fixed diff --git a/ee/lib/api/geo_nodes.rb b/ee/lib/api/geo_nodes.rb index 8875d02e772101ba2e56bd771c479d3ad4593ee3..4989c313f14c7be2d16ecf3d50126e417cd69e58 100644 --- a/ee/lib/api/geo_nodes.rb +++ b/ee/lib/api/geo_nodes.rb @@ -48,7 +48,7 @@ class GeoNodes < Grape::API get '/current/failures' do geo_node = Gitlab::Geo.current_node - not_found('Geo node not found') unless geo_node + not_found!('Geo node not found') unless geo_node finder = ::Geo::ProjectRegistryFinder.new(current_node: geo_node) project_registries = paginate(finder.find_failed_project_registries(params[:type])) diff --git a/qa/qa/ee/scenario/test/geo.rb b/qa/qa/ee/scenario/test/geo.rb index 49f0a7427333163e7d7424adb921ae28d31d2436..5fb6ae5e6fda534d73f0db2b995ff1a401200b53 100644 --- a/qa/qa/ee/scenario/test/geo.rb +++ b/qa/qa/ee/scenario/test/geo.rb @@ -11,7 +11,7 @@ class Geo < QA::Scenario::Template attribute :geo_secondary_name, '--secondary-name SECONDARY_NAME' attribute :geo_skip_setup?, '--without-setup' - def perform(options, *files) + def perform(options, *rspec_options) unless options[:geo_skip_setup?] Geo::Primary.act do add_license @@ -27,6 +27,7 @@ def perform(options, *files) Specs::Runner.perform do |specs| specs.tty = true specs.tags = %w[geo] + specs.options = rspec_options.any? ? rspec_options : 'qa/specs/features' end end diff --git a/qa/qa/runtime/api.rb b/qa/qa/runtime/api.rb index e2a096b971d97b7b13071c44befc9b40a9bc1a82..3f53b6b7153d28f8846608a0cf5e6657334a7a98 100644 --- a/qa/qa/runtime/api.rb +++ b/qa/qa/runtime/api.rb @@ -6,8 +6,9 @@ module API class Client attr_reader :address - def initialize(address = :gitlab) + def initialize(address = :gitlab, personal_access_token = nil) @address = address + @personal_access_token = personal_access_token end def personal_access_token diff --git a/qa/qa/specs/features/ee/api/geo_nodes_spec.rb b/qa/qa/specs/features/ee/api/geo_nodes_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8531761667c62f236f112dd76b61e42642317493 --- /dev/null +++ b/qa/qa/specs/features/ee/api/geo_nodes_spec.rb @@ -0,0 +1,231 @@ +module QA + describe 'Geo Nodes API' do + before(:all) do + get_personal_access_token + end + + shared_examples 'retrieving configuration about Geo nodes' do + scenario 'GET /geo_nodes' do + get api_endpoint('/geo_nodes') + + expect_status(200) + expect(json_body.size).to be >= 2 + expect_json('?', primary: true) + expect_json_types('*', primary: :boolean, current: :boolean, + files_max_capacity: :integer, repos_max_capacity: :integer, + clone_protocol: :string, _links: :object) + end + + scenario 'GET /geo_nodes/:id' do + get api_endpoint("/geo_nodes/#{geo_node[:id]}") + + expect_status(200) + expect(json_body).to eq geo_node + end + end + + shared_examples 'retrieving status about all Geo nodes' do + scenario 'GET /geo_nodes/status' do + get api_endpoint('/geo_nodes/status') + + expect_status(200) + expect(json_body.size).to be >= 2 + + # only need to check that some of the key values are there + expect_json_types('*', health: :string, + attachments_count: :integer, + db_replication_lag_seconds: :integer_or_null, + lfs_objects_count: :integer, + job_artifacts_count: :integer, + repositories_count: :integer, + wikis_count: :integer, + replication_slots_count: :integer_or_null, + version: :string_or_null) + end + end + + shared_examples 'retrieving status about a specific Geo node' do + scenario 'GET /geo_nodes/:id/status of primary node' do + get api_endpoint("/geo_nodes/#{@primary_node[:id]}/status") + + expect_status(200) + expect_json(geo_node_id: @primary_node[:id]) + end + + scenario 'GET /geo_nodes/:id/status of secondary node' do + get api_endpoint("/geo_nodes/#{@secondary_node[:id]}/status") + + expect_status(200) + expect_json(geo_node_id: @secondary_node[:id]) + end + + scenario 'GET /geo_nodes/:id/status of an invalid node' do + get api_endpoint("/geo_nodes/1000/status") + + expect_status(404) + end + end + + shared_examples 'retrieving project sync failures ocurred on the current node' do + scenario 'GET /geo_nodes/current/failures' do + get api_endpoint("/geo_nodes/current/failures") + + expect_status(200) + expect(json_body).to be_an Array + end + end + + feature 'Geo Nodes API on primary node', :geo do + before(:context) do + fetch_nodes(:geo_primary) + end + + include_examples 'retrieving configuration about Geo nodes' do + let(:geo_node) { @primary_node } + end + + include_examples 'retrieving status about all Geo nodes' + include_examples 'retrieving status about a specific Geo node' + + feature 'editing a Geo node' do + scenario 'PUT /geo_nodes/:id for primary node' do + put api_endpoint("/geo_nodes/#{@primary_node[:id]}"), + { params: { files_max_capacity: 1000 } } + + expect_status(403) + end + + scenario 'PUT /geo_nodes/:id for secondary node' do + endpoint = api_endpoint("/geo_nodes/#{@secondary_node[:id]}") + new_attributes = { enabled: false, files_max_capacity: 1000, repos_max_capacity: 2000 } + + put endpoint, new_attributes + + expect_status(200) + expect_json(new_attributes) + + # restore the original values + put endpoint, { enabled: @secondary_node[:enabled], + files_max_capacity: @secondary_node[:files_max_capacity], + repos_max_capacity: @secondary_node[:repos_max_capacity] } + + expect_status(200) + end + + scenario 'PUT /geo_nodes/:id for an invalid node' do + put api_endpoint("/geo_nodes/1000"), + { params: { files_max_capacity: 1000 } } + + expect_status(404) + end + end + + feature 'repairing a Geo node' do + scenario 'POST /geo_nodes/:id/repair for primary node' do + post api_endpoint("/geo_nodes/#{@primary_node[:id]}/repair") + + expect_status(200) + expect_json(geo_node_id: @primary_node[:id]) + end + + scenario 'POST /geo_nodes/:id/repair for secondary node' do + post api_endpoint("/geo_nodes/#{@secondary_node[:id]}/repair") + + expect_status(200) + expect_json(geo_node_id: @secondary_node[:id]) + end + + scenario 'POST /geo_nodes/:id/repair for an invalid node' do + post api_endpoint("/geo_nodes/1000/repair") + + expect_status(404) + end + end + end + + feature 'Geo Nodes API on secondary node', :geo do + before(:context) do + fetch_nodes(:geo_secondary) + end + + include_examples 'retrieving configuration about Geo nodes' do + let(:geo_node) { @nodes.first } + end + + include_examples 'retrieving status about all Geo nodes' + include_examples 'retrieving status about a specific Geo node' + include_examples 'retrieving project sync failures ocurred on the current node' + + scenario 'GET /geo_nodes is not current' do + get api_endpoint('/geo_nodes') + + expect_status(200) + expect_json('?', current: false) + end + + feature 'editing a Geo node' do + scenario 'PUT /geo_nodes/:id for primary node' do + put api_endpoint("/geo_nodes/#{@primary_node[:id]}"), + { params: { files_max_capacity: 1000 } } + + expect_status(403) + end + + scenario 'PUT /geo_nodes/:id for secondary node' do + put api_endpoint("/geo_nodes/#{@secondary_node[:id]}"), + { params: { files_max_capacity: 1000 } } + + expect_status(403) + end + + scenario 'PUT /geo_nodes/:id for an invalid node' do + put api_endpoint('/geo_nodes/1000'), + { params: { files_max_capacity: 1000 } } + + expect_status(403) + end + end + + feature 'repairing a Geo node' do + scenario 'POST /geo_nodes/:id/repair for primary node' do + post api_endpoint("/geo_nodes/#{@primary_node[:id]}/repair") + + expect_status(403) + end + + scenario 'POST /geo_nodes/:id/repair for secondary node' do + post api_endpoint("/geo_nodes/#{@secondary_node[:id]}/repair") + + expect_status(403) + end + + scenario 'POST /geo_nodes/:id/repair for an invalid node' do + post api_endpoint('/geo_nodes/1000/repair') + + expect_status(403) + end + end + end + + def api_endpoint(endpoint) + QA::Runtime::API::Request.new(@api_client, endpoint).url + end + + def fetch_nodes(node_type) + @api_client = Runtime::API::Client.new(node_type, @personal_access_token) + + get api_endpoint('/geo_nodes') + + @nodes = json_body + @primary_node = @nodes.detect { |node| node[:primary] == true } + @secondary_node = @nodes.detect { |node| node[:primary] == false } + end + + # go to the primary and create a personal_access_token, which will be used + # for accessing both the primary and secondary + def get_personal_access_token + api_client = Runtime::API::Client.new(:geo_primary) + @personal_access_token = api_client.personal_access_token + end + end +end