Commit f568fe65 authored by Michael Kozono's avatar Michael Kozono

Merge branch '10586-add-flag-to-object-storage' into 'master'

Make Object Storage synchronization in Geo Nodes configurable via Admin UI

See merge request gitlab-org/gitlab-ee!15139
parents f5e38419 05e263e9
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddObjectStorageFlagToGeoNode < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :geo_nodes, :sync_object_storage, :boolean, default: false
end
def down
remove_column :geo_nodes, :sync_object_storage
end
end
...@@ -1456,6 +1456,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do ...@@ -1456,6 +1456,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do
t.integer "container_repositories_max_capacity", default: 10, null: false t.integer "container_repositories_max_capacity", default: 10, null: false
t.datetime_with_timezone "created_at" t.datetime_with_timezone "created_at"
t.datetime_with_timezone "updated_at" t.datetime_with_timezone "updated_at"
t.boolean "sync_object_storage", default: false, null: false
t.index ["access_key"], name: "index_geo_nodes_on_access_key" t.index ["access_key"], name: "index_geo_nodes_on_access_key"
t.index ["name"], name: "index_geo_nodes_on_name", unique: true t.index ["name"], name: "index_geo_nodes_on_name", unique: true
t.index ["primary"], name: "index_geo_nodes_on_primary" t.index ["primary"], name: "index_geo_nodes_on_primary"
......
...@@ -10,7 +10,7 @@ GET /geo_nodes ...@@ -10,7 +10,7 @@ GET /geo_nodes
``` ```
```bash ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes
``` ```
Example response: Example response:
...@@ -29,7 +29,13 @@ Example response: ...@@ -29,7 +29,13 @@ Example response:
"repos_max_capacity": 25, "repos_max_capacity": 25,
"container_repositories_max_capacity": 10, "container_repositories_max_capacity": 10,
"verification_max_capacity": 100, "verification_max_capacity": 100,
"clone_protocol": "http" "clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit",
"_links": {
"self": "https://primary.example.com/api/v4/geo_nodes/1",
"status":"https://primary.example.com/api/v4/geo_nodes/1/status",
"repair":"https://primary.example.com/api/v4/geo_nodes/1/repair"
}
}, },
{ {
"id": 2, "id": 2,
...@@ -43,7 +49,15 @@ Example response: ...@@ -43,7 +49,15 @@ Example response:
"repos_max_capacity": 25, "repos_max_capacity": 25,
"container_repositories_max_capacity": 10, "container_repositories_max_capacity": 10,
"verification_max_capacity": 100, "verification_max_capacity": 100,
"clone_protocol": "http" "sync_object_storage": true,
"clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit",
"web_geo_projects_url": "https://secondary.example.com/admin/geo/projects",
"_links": {
"self":"https://primary.example.com/api/v4/geo_nodes/2",
"status":"https://primary.example.com/api/v4/geo_nodes/2/status",
"repair":"https://primary.example.com/api/v4/geo_nodes/2/repair"
}
} }
] ]
``` ```
...@@ -55,7 +69,7 @@ GET /geo_nodes/:id ...@@ -55,7 +69,7 @@ GET /geo_nodes/:id
``` ```
```bash ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/1 curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/1
``` ```
Example response: Example response:
...@@ -73,7 +87,13 @@ Example response: ...@@ -73,7 +87,13 @@ Example response:
"repos_max_capacity": 25, "repos_max_capacity": 25,
"container_repositories_max_capacity": 10, "container_repositories_max_capacity": 10,
"verification_max_capacity": 100, "verification_max_capacity": 100,
"clone_protocol": "http" "clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit",
"_links": {
"self": "https://primary.example.com/api/v4/geo_nodes/1",
"status":"https://primary.example.com/api/v4/geo_nodes/1/status",
"repair":"https://primary.example.com/api/v4/geo_nodes/1/repair"
}
} }
``` ```
...@@ -88,7 +108,7 @@ PUT /geo_nodes/:id ...@@ -88,7 +108,7 @@ PUT /geo_nodes/:id
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
|----------------------|---------|-----------|---------------------------------------------------------------------------| |-----------------------------|---------|-----------|---------------------------------------------------------------------------|
| `id` | integer | yes | The ID of the Geo node. | | `id` | integer | yes | The ID of the Geo node. |
| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. | | `enabled` | boolean | no | Flag indicating if the Geo node is enabled. |
| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url`. | | `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url`. |
...@@ -98,6 +118,7 @@ PUT /geo_nodes/:id ...@@ -98,6 +118,7 @@ PUT /geo_nodes/:id
| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. | | `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. |
| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. | | `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. |
| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. | | `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. |
| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. |
Example response: Example response:
...@@ -114,7 +135,15 @@ Example response: ...@@ -114,7 +135,15 @@ Example response:
"repos_max_capacity": 25, "repos_max_capacity": 25,
"container_repositories_max_capacity": 10, "container_repositories_max_capacity": 10,
"verification_max_capacity": 100, "verification_max_capacity": 100,
"clone_protocol": "http" "sync_object_storage": true,
"clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit",
"web_geo_projects_url": "https://secondary.example.com/admin/geo/projects",
"_links": {
"self":"https://primary.example.com/api/v4/geo_nodes/2",
"status":"https://primary.example.com/api/v4/geo_nodes/2/status",
"repair":"https://primary.example.com/api/v4/geo_nodes/2/repair"
}
} }
``` ```
...@@ -158,7 +187,13 @@ Example response: ...@@ -158,7 +187,13 @@ Example response:
"repos_max_capacity": 25, "repos_max_capacity": 25,
"container_repositories_max_capacity": 10, "container_repositories_max_capacity": 10,
"verification_max_capacity": 100, "verification_max_capacity": 100,
"clone_protocol": "http" "clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit",
"_links": {
"self": "https://primary.example.com/api/v4/geo_nodes/1",
"status":"https://primary.example.com/api/v4/geo_nodes/1/status",
"repair":"https://primary.example.com/api/v4/geo_nodes/1/repair"
}
} }
``` ```
...@@ -169,7 +204,7 @@ GET /geo_nodes/status ...@@ -169,7 +204,7 @@ GET /geo_nodes/status
``` ```
```bash ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/status curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/status
``` ```
Example response: Example response:
...@@ -320,7 +355,7 @@ GET /geo_nodes/:id/status ...@@ -320,7 +355,7 @@ GET /geo_nodes/:id/status
``` ```
```bash ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/2/status curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/2/status
``` ```
Example response: Example response:
...@@ -394,7 +429,7 @@ GET /geo_nodes/current/failures ...@@ -394,7 +429,7 @@ GET /geo_nodes/current/failures
This endpoint uses [Pagination](README.md#pagination). This endpoint uses [Pagination](README.md#pagination).
```bash ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/current/failures curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/current/failures
``` ```
Example response: Example response:
......
...@@ -22,7 +22,7 @@ export default function geoNodeForm() { ...@@ -22,7 +22,7 @@ export default function geoNodeForm() {
const $container = $('.js-geo-node-form'); const $container = $('.js-geo-node-form');
const $namespaces = $('.js-hide-if-geo-primary', $container); const $namespaces = $('.js-hide-if-geo-primary', $container);
const $reverification = $('.js-hide-if-geo-secondary', $container); const $reverification = $('.js-hide-if-geo-secondary', $container);
const $primaryCheckbox = $('input[type="checkbox"]', $container); const $primaryCheckbox = $('input#geo_node_primary', $container);
const $selectiveSyncTypeSelect = $('.js-geo-node-selective-sync-type', $container); const $selectiveSyncTypeSelect = $('.js-geo-node-selective-sync-type', $container);
const $select2Dropdown = $('.js-geo-node-namespaces', $container); const $select2Dropdown = $('.js-geo-node-namespaces', $container);
const $syncByNamespaces = $('.js-sync-by-namespace', $container); const $syncByNamespaces = $('.js-sync-by-namespace', $container);
......
...@@ -58,6 +58,7 @@ class Admin::Geo::NodesController < Admin::Geo::ApplicationController ...@@ -58,6 +58,7 @@ class Admin::Geo::NodesController < Admin::Geo::ApplicationController
:verification_max_capacity, :verification_max_capacity,
:minimum_reverification_interval, :minimum_reverification_interval,
:container_repositories_max_capacity, :container_repositories_max_capacity,
:sync_object_storage,
selective_sync_shards: [] selective_sync_shards: []
) )
end end
......
...@@ -10,12 +10,6 @@ ...@@ -10,12 +10,6 @@
= form.text_field :url, class: 'form-control qa-node-url-field' = form.text_field :url, class: 'form-control qa-node-url-field'
.form-text.text-muted= _('The user-facing URL of the Geo node') .form-text.text-muted= _('The user-facing URL of the Geo node')
.form-group.col-sm-6.js-internal-url{ class: ('hidden' unless geo_node.primary?) }
= form.label :internal_url, s_('Geo|Internal URL (optional)'), class: 'font-weight-bold'
= form.text_field :internal_url, class: 'form-control'
.form-text.text-muted= s_('Geo|The URL defined on the primary node that secondary nodes should use to contact it. Defaults to URL')
.form-group.row .form-group.row
.col-sm-10 .col-sm-10
.form-check .form-check
...@@ -23,6 +17,12 @@ ...@@ -23,6 +17,12 @@
= form.label :primary, class: 'form-check-label' do = form.label :primary, class: 'form-check-label' do
%span= s_('Geo|This is a primary node') %span= s_('Geo|This is a primary node')
.form-row.form-group.js-internal-url{ class: ('hidden' unless geo_node.primary?) }
.col-sm-6
= form.label :internal_url, s_('Geo|Internal URL (optional)'), class: 'font-weight-bold'
= form.text_field :internal_url, class: 'form-control'
.form-text.text-muted= s_('Geo|The URL defined on the primary node that secondary nodes should use to contact it. Defaults to URL')
.form-row.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) } .form-row.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-4 .col-sm-4
= form.label :selective_sync_type, s_('Geo|Selective synchronization'), class: 'font-weight-bold' = form.label :selective_sync_type, s_('Geo|Selective synchronization'), class: 'font-weight-bold'
...@@ -69,3 +69,14 @@ ...@@ -69,3 +69,14 @@
= form.label :minimum_reverification_interval, s_('Geo|Re-verification interval'), class: 'font-weight-bold' = form.label :minimum_reverification_interval, s_('Geo|Re-verification interval'), class: 'font-weight-bold'
= form.number_field :minimum_reverification_interval, class: 'form-control col-sm-2', min: 1 = form.number_field :minimum_reverification_interval, class: 'form-control col-sm-2', min: 1
.form-text.text-muted= s_('Geo|Control the minimum interval in days that a repository should be reverified for this primary node') .form-text.text-muted= s_('Geo|Control the minimum interval in days that a repository should be reverified for this primary node')
- if ::Feature.enabled?(:geo_object_storage_replication)
.form-group.row.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-10
= form.label :sync_object_storage, _('Object Storage replication'), class: 'label-bold'
.form-check
= form.check_box :sync_object_storage, class: 'form-check-input'
= form.label :sync_object_storage, class: 'form-check-label' do
%span= s_('Geo|Allow this secondary node to replicate content on Object Storage')
.form-text.text-muted= s_('Geo|If enabled, and if object storage is enabled, GitLab will handle Object Storage replication using Geo')
---
title: 'Geo: Make Object Storage synchronization in Geo Nodes configurable via Admin UI'
merge_request: 15000
author:
type: added
...@@ -147,6 +147,7 @@ module API ...@@ -147,6 +147,7 @@ module API
optional :repos_max_capacity, type: Integer, desc: 'Control the maximum concurrency of repository backfill for this secondary node' optional :repos_max_capacity, type: Integer, desc: 'Control the maximum concurrency of repository backfill for this secondary node'
optional :verification_max_capacity, type: Integer, desc: 'Control the maximum concurrency of repository verification for this node' optional :verification_max_capacity, type: Integer, desc: 'Control the maximum concurrency of repository verification for this node'
optional :container_repositories_max_capacity, type: Integer, desc: 'Control the maximum concurrency of container repository sync for this node' optional :container_repositories_max_capacity, type: Integer, desc: 'Control the maximum concurrency of container repository sync for this node'
optional :sync_object_storage, type: Boolean, desc: 'Flag indicating if the secondary Geo node will replicate blobs in Object Storage'
end end
put do put do
not_found!('GeoNode') unless geo_node not_found!('GeoNode') unless geo_node
......
...@@ -485,6 +485,7 @@ module EE ...@@ -485,6 +485,7 @@ module EE
expose :repos_max_capacity expose :repos_max_capacity
expose :verification_max_capacity expose :verification_max_capacity
expose :container_repositories_max_capacity expose :container_repositories_max_capacity
expose :sync_object_storage, if: ->(geo_node, _) { ::Feature.enabled?(:geo_object_storage_replication) && geo_node.secondary? }
# Retained for backwards compatibility. Remove in API v5 # Retained for backwards compatibility. Remove in API v5
expose :clone_protocol do |_record, _options| expose :clone_protocol do |_record, _options|
......
...@@ -9,10 +9,12 @@ FactoryBot.define do ...@@ -9,10 +9,12 @@ FactoryBot.define do
end end
primary false primary false
sync_object_storage true
trait :primary do trait :primary do
primary true primary true
minimum_reverification_interval 7 minimum_reverification_interval 7
sync_object_storage false
end end
end end
end end
...@@ -89,16 +89,48 @@ describe 'admin Geo Nodes', :js, :geo do ...@@ -89,16 +89,48 @@ describe 'admin Geo Nodes', :js, :geo do
end end
end end
it 'changes re-verification interval field visibility based on primary node checkbox' do it 'toggles the visibility of secondary only params based on primary node checkbox' do
expect(page).not_to have_field('Re-verification interval') primary_only_fields = [
'Internal URL (optional)',
'Re-verification interval'
]
secondary_only_fields = [
'Selective synchronization',
'Repository sync capacity',
'File sync capacity',
'Object Storage replication'
]
expect(page).to have_unchecked_field('This is a primary node')
primary_only_fields.each do |field|
expect(page).to have_field(field, visible: false)
end
secondary_only_fields.each do |field|
expect(page).to have_field(field)
end
check 'This is a primary node' check 'This is a primary node'
expect(page).to have_field('Re-verification interval') primary_only_fields.each do |field|
expect(page).to have_field(field)
end
secondary_only_fields.each do |field|
expect(page).to have_field(field, visible: false)
end
uncheck 'This is a primary node' uncheck 'This is a primary node'
expect(page).not_to have_field('Re-verification interval') primary_only_fields.each do |field|
expect(page).to have_field(field, visible: false)
end
secondary_only_fields.each do |field|
expect(page).to have_field(field)
end
end end
it 'returns an error message when a duplicate primary is added' do it 'returns an error message when a duplicate primary is added' do
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
"verification_max_capacity", "verification_max_capacity",
"container_repositories_max_capacity", "container_repositories_max_capacity",
"clone_protocol", "clone_protocol",
"web_edit_url",
"_links" "_links"
], ],
"properties" : { "properties" : {
...@@ -26,6 +27,7 @@ ...@@ -26,6 +27,7 @@
"repos_max_capacity": { "type": "integer" }, "repos_max_capacity": { "type": "integer" },
"verification_max_capacity": { "type": "integer" }, "verification_max_capacity": { "type": "integer" },
"container_repositories_max_capacity": { "type": "integer" }, "container_repositories_max_capacity": { "type": "integer" },
"sync_object_storage" : { "type": "boolean" },
"clone_protocol": { "type": "string" }, "clone_protocol": { "type": "string" },
"web_edit_url": { "type": "string" }, "web_edit_url": { "type": "string" },
"web_geo_projects_url" : { "type": ["string", "null"] }, "web_geo_projects_url" : { "type": ["string", "null"] },
......
...@@ -15,12 +15,13 @@ export const mockNodes = [ ...@@ -15,12 +15,13 @@ export const mockNodes = [
files_max_capacity: 10, files_max_capacity: 10,
repos_max_capacity: 25, repos_max_capacity: 25,
container_repositories_max_capacity: 10, container_repositories_max_capacity: 10,
verification_max_capacity: 100,
clone_protocol: 'http', clone_protocol: 'http',
web_edit_url: 'http://127.0.0.1:3001/admin/geo/nodes/1/edit',
_links: { _links: {
self: 'http://127.0.0.1:3001/api/v4/geo_nodes/1', self: 'http://127.0.0.1:3001/api/v4/geo_nodes/1',
repair: 'http://127.0.0.1:3001/api/v4/geo_nodes/1/repair', repair: 'http://127.0.0.1:3001/api/v4/geo_nodes/1/repair',
status: 'http://127.0.0.1:3001/api/v4/geo_nodes/1/status', status: 'http://127.0.0.1:3001/api/v4/geo_nodes/1/status',
web_edit: 'http://127.0.0.1:3001/admin/geo/nodes/1/edit',
}, },
}, },
{ {
...@@ -32,12 +33,14 @@ export const mockNodes = [ ...@@ -32,12 +33,14 @@ export const mockNodes = [
files_max_capacity: 10, files_max_capacity: 10,
repos_max_capacity: 25, repos_max_capacity: 25,
container_repositories_max_capacity: 10, container_repositories_max_capacity: 10,
verification_max_capacity: 100,
sync_object_storage: true,
clone_protocol: 'http', clone_protocol: 'http',
web_edit_url: 'http://127.0.0.1:3001/admin/geo/nodes/1/edit',
_links: { _links: {
self: 'http://127.0.0.1:3001/api/v4/geo_nodes/2', self: 'http://127.0.0.1:3001/api/v4/geo_nodes/2',
repair: 'http://127.0.0.1:3001/api/v4/geo_nodes/2/repair', repair: 'http://127.0.0.1:3001/api/v4/geo_nodes/2/repair',
status: 'http://127.0.0.1:3001/api/v4/geo_nodes/2/status', status: 'http://127.0.0.1:3001/api/v4/geo_nodes/2/status',
web_edit: 'http://127.0.0.1:3001/admin/geo/nodes/2/edit',
}, },
}, },
]; ];
......
...@@ -118,6 +118,7 @@ describe GeoNode, :geo, type: :model do ...@@ -118,6 +118,7 @@ describe GeoNode, :geo, type: :model do
:repos_max_capacity | 25 :repos_max_capacity | 25
:files_max_capacity | 10 :files_max_capacity | 10
:container_repositories_max_capacity | 10 :container_repositories_max_capacity | 10
:sync_object_storage | false
end end
with_them do with_them do
......
...@@ -6814,6 +6814,9 @@ msgstr "" ...@@ -6814,6 +6814,9 @@ msgstr ""
msgid "Geo|All projects are being scheduled for re-verify" msgid "Geo|All projects are being scheduled for re-verify"
msgstr "" msgstr ""
msgid "Geo|Allow this secondary node to replicate content on Object Storage"
msgstr ""
msgid "Geo|Batch operations" msgid "Geo|Batch operations"
msgstr "" msgstr ""
...@@ -6853,6 +6856,9 @@ msgstr "" ...@@ -6853,6 +6856,9 @@ msgstr ""
msgid "Geo|Groups to synchronize" msgid "Geo|Groups to synchronize"
msgstr "" msgstr ""
msgid "Geo|If enabled, and if object storage is enabled, GitLab will handle Object Storage replication using Geo"
msgstr ""
msgid "Geo|In sync" msgid "Geo|In sync"
msgstr "" msgstr ""
...@@ -10112,6 +10118,9 @@ msgstr "" ...@@ -10112,6 +10118,9 @@ msgstr ""
msgid "OK" msgid "OK"
msgstr "" msgstr ""
msgid "Object Storage replication"
msgstr ""
msgid "Object does not exist on the server or you don't have permissions to access it" msgid "Object does not exist on the server or you don't have permissions to access it"
msgstr "" msgstr ""
......
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