Commit 38bea69b authored by Michael Kozono's avatar Michael Kozono Committed by Nick Thomas

Add ability to set `alternate_url` in UI

Add the form field, and allow the data to flow through to the model
just like the regular `url` field.
parent 95178fa1
...@@ -1324,6 +1324,7 @@ ActiveRecord::Schema.define(version: 20190305162221) do ...@@ -1324,6 +1324,7 @@ ActiveRecord::Schema.define(version: 20190305162221) do
t.text "selective_sync_shards" t.text "selective_sync_shards"
t.integer "verification_max_capacity", default: 100, null: false t.integer "verification_max_capacity", default: 100, null: false
t.integer "minimum_reverification_interval", default: 7, null: false t.integer "minimum_reverification_interval", default: 7, null: false
t.string "alternate_url"
t.index ["access_key"], name: "index_geo_nodes_on_access_key", using: :btree t.index ["access_key"], name: "index_geo_nodes_on_access_key", using: :btree
t.index ["primary"], name: "index_geo_nodes_on_primary", using: :btree t.index ["primary"], name: "index_geo_nodes_on_primary", using: :btree
t.index ["url"], name: "index_geo_nodes_on_url", unique: true, using: :btree t.index ["url"], name: "index_geo_nodes_on_url", unique: true, using: :btree
......
...@@ -31,6 +31,7 @@ Example response: ...@@ -31,6 +31,7 @@ Example response:
{ {
"id": 2, "id": 2,
"url": "https://secondary.example.com/", "url": "https://secondary.example.com/",
"alternate_url": "https://alternate.example.com/",
"primary": false, "primary": false,
"enabled": true, "enabled": true,
"current": false, "current": false,
...@@ -83,6 +84,7 @@ PUT /geo_nodes/:id ...@@ -83,6 +84,7 @@ PUT /geo_nodes/:id
| `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. |
| `url` | string | no | The URL to connect to the Geo node. | | `url` | string | no | The URL to connect to the Geo node. |
| `alternate_url` | string | no | Allows users to log in to the secondary at an alternate URL (required for OAuth) |
| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. | | `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. |
| `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. |
...@@ -93,6 +95,7 @@ Example response: ...@@ -93,6 +95,7 @@ Example response:
{ {
"id": 1, "id": 1,
"url": "https://secondary.example.com/", "url": "https://secondary.example.com/",
"alternate_url": "https://alternate.example.com/",
"primary": false, "primary": false,
"enabled": true, "enabled": true,
"current": true, "current": true,
......
...@@ -27,6 +27,7 @@ Secondaries have a number of additional settings available: ...@@ -27,6 +27,7 @@ Secondaries have a number of additional settings available:
| Selective synchronization | Enable Geo [selective sync](../../administration/geo/replication/configuration.md#selective-synchronization) for this secondary. | | Selective synchronization | Enable Geo [selective sync](../../administration/geo/replication/configuration.md#selective-synchronization) for this secondary. |
| Repository sync capacity | Number of concurrent requests this secondary will make to the primary when backfilling repositories. | | Repository sync capacity | Number of concurrent requests this secondary will make to the primary when backfilling repositories. |
| File sync capacity | Number of concurrent requests this secondary will make to the primary when backfilling files. | | File sync capacity | Number of concurrent requests this secondary will make to the primary when backfilling files. |
| Alternate URL | Allows users to log in to the secondary at an alternate URL (required for OAuth) |
## Geo backfill ## Geo backfill
...@@ -45,3 +46,13 @@ the limits are configurable - if your primary node has lots of surplus capacity, ...@@ -45,3 +46,13 @@ the limits are configurable - if your primary node has lots of surplus capacity,
you can increase the values to complete backfill in a shorter time. If it's you can increase the values to complete backfill in a shorter time. If it's
under heavy load and backfill is reducing its availability for normal requests, under heavy load and backfill is reducing its availability for normal requests,
you can decrease them. you can decrease them.
## Multiple secondaries behind a load balancer
Secondaries are authenticated via OAuth with the primary. For security, the
primary does not allow redirecting back to an arbitrary URL. If you want to
allow users to log in to secondaries at a common name/load balancer URL, then
this URL must be specified as the "Alternate URL" on every secondary behind it.
Additionally, the load balancer should use sticky sessions, since users must
authenticate each first request to each secondary.
...@@ -121,7 +121,7 @@ export default { ...@@ -121,7 +121,7 @@ export default {
</script> </script>
<template> <template>
<div class="node-detail-item prepend-top-15 prepend-left-10"> <div class="node-detail-item">
<div class="node-detail-title"> <div class="node-detail-title">
<span> {{ itemTitle }} </span> <span> {{ itemTitle }} </span>
<icon <icon
......
...@@ -74,7 +74,11 @@ export default { ...@@ -74,7 +74,11 @@ export default {
:node-details="nodeDetails" :node-details="nodeDetails"
:node-type-primary="node.primary" :node-type-primary="node.primary"
/> />
<node-details-section-other :node-details="nodeDetails" :node-type-primary="node.primary" /> <node-details-section-other
:node="node"
:node-details="nodeDetails"
:node-type-primary="node.primary"
/>
<div v-if="hasError || hasVersionMismatch" class="node-health-message-container"> <div v-if="hasError || hasVersionMismatch" class="node-health-message-container">
<p class="node-health-message">{{ errorMessage }}</p> <p class="node-health-message">{{ errorMessage }}</p>
</div> </div>
......
...@@ -17,6 +17,10 @@ export default { ...@@ -17,6 +17,10 @@ export default {
}, },
mixins: [DetailsSectionMixin], mixins: [DetailsSectionMixin],
props: { props: {
node: {
type: Object,
required: true,
},
nodeDetails: { nodeDetails: {
type: Object, type: Object,
required: true, required: true,
...@@ -65,6 +69,12 @@ export default { ...@@ -65,6 +69,12 @@ export default {
itemValueType: VALUE_TYPE.PLAIN, itemValueType: VALUE_TYPE.PLAIN,
cssClass: this.storageShardsCssClass, cssClass: this.storageShardsCssClass,
}, },
{
itemTitle: s__('GeoNodes|Alternate URL'),
itemValue: this.node.alternateUrl,
itemValueType: VALUE_TYPE.PLAIN,
cssClass: 'node-detail-value-bold',
},
]; ];
}, },
storageShardsStatus() { storageShardsStatus() {
...@@ -81,6 +91,14 @@ export default { ...@@ -81,6 +91,14 @@ export default {
? `${cssClass} node-detail-value-error` ? `${cssClass} node-detail-value-error`
: cssClass; : cssClass;
}, },
sectionItemsContainerClasses() {
const { nodeTypePrimary, showSectionItems } = this;
return {
'col-md-6 prepend-left-15': nodeTypePrimary,
'row col-md-12 prepend-left-10': !nodeTypePrimary,
'd-flex': showSectionItems && !nodeTypePrimary,
};
},
}, },
methods: { methods: {
handleSectionToggle(toggleState) { handleSectionToggle(toggleState) {
...@@ -100,15 +118,19 @@ export default { ...@@ -100,15 +118,19 @@ export default {
</div> </div>
<div <div
v-show="showSectionItems" v-show="showSectionItems"
class="col-md-6 prepend-left-15 prepend-top-10 section-items-container" :class="sectionItemsContainerClasses"
class="prepend-top-10 section-items-container"
> >
<geo-node-detail-item <geo-node-detail-item
:item-title="s__('GeoNodes|Storage config')" v-for="(nodeDetailItem, index) in nodeDetailItems"
:item-value="storageShardsStatus" :key="index"
:item-value-type="$options.valueType.PLAIN" :class="{ 'prepend-top-15 prepend-left-10': nodeTypePrimary, 'col-sm-3': !nodeTypePrimary }"
:css-class="nodeDetailItem.cssClass"
:item-title="nodeDetailItem.itemTitle"
:item-value="nodeDetailItem.itemValue"
:item-value-type="nodeDetailItem.itemValueType"
:item-value-stale="statusInfoStale" :item-value-stale="statusInfoStale"
:item-value-stale-tooltip="statusInfoStaleMessage" :item-value-stale-tooltip="statusInfoStaleMessage"
:css-class="storageShardsCssClass"
/> />
</div> </div>
</div> </div>
......
...@@ -141,6 +141,7 @@ export default { ...@@ -141,6 +141,7 @@ export default {
:item-value-stale-tooltip="statusInfoStaleMessage" :item-value-stale-tooltip="statusInfoStaleMessage"
:custom-type="nodeDetailItem.customType" :custom-type="nodeDetailItem.customType"
:event-type-log-status="nodeDetailItem.eventTypeLogStatus" :event-type-log-status="nodeDetailItem.eventTypeLogStatus"
class="prepend-top-15 prepend-left-10"
/> />
</div> </div>
</div> </div>
......
...@@ -136,6 +136,7 @@ export default { ...@@ -136,6 +136,7 @@ export default {
:failure-label="nodeDetailItem.failureLabel" :failure-label="nodeDetailItem.failureLabel"
:custom-type="nodeDetailItem.customType" :custom-type="nodeDetailItem.customType"
:help-info="nodeDetailItem.helpInfo" :help-info="nodeDetailItem.helpInfo"
class="prepend-top-15 prepend-left-10"
/> />
</div> </div>
</template> </template>
......
...@@ -48,6 +48,7 @@ export default class GeoNodesStore { ...@@ -48,6 +48,7 @@ export default class GeoNodesStore {
primary, primary,
current, current,
enabled, enabled,
alternateUrl: rawNode.alternate_url || '',
nodeActionActive: false, nodeActionActive: false,
basePath: rawNode._links.self, basePath: rawNode._links.self,
repairPath: rawNode._links.repair, repairPath: rawNode._links.repair,
......
...@@ -50,6 +50,7 @@ class Admin::Geo::NodesController < Admin::ApplicationController ...@@ -50,6 +50,7 @@ class Admin::Geo::NodesController < Admin::ApplicationController
def geo_node_params def geo_node_params
params.require(:geo_node).permit( params.require(:geo_node).permit(
:url, :url,
:alternate_url,
:primary, :primary,
:selective_sync_type, :selective_sync_type,
:namespace_ids, :namespace_ids,
......
...@@ -18,7 +18,8 @@ class GeoNode < ActiveRecord::Base ...@@ -18,7 +18,8 @@ class GeoNode < ActiveRecord::Base
primary: false primary: false
validates :url, presence: true, uniqueness: { case_sensitive: false } validates :url, presence: true, uniqueness: { case_sensitive: false }
validate :check_url_is_valid validate :url_is_http
validate :alternate_url_is_http
validates :primary, uniqueness: { message: 'node already exists' }, if: :primary validates :primary, uniqueness: { message: 'node already exists' }, if: :primary
validates :enabled, if: :primary, acceptance: { message: 'Geo primary node cannot be disabled' } validates :enabled, if: :primary, acceptance: { message: 'Geo primary node cannot be disabled' }
...@@ -121,24 +122,33 @@ class GeoNode < ActiveRecord::Base ...@@ -121,24 +122,33 @@ class GeoNode < ActiveRecord::Base
end end
def url def url
value = read_attribute(:url) read_with_ending_slash(:url)
value += '/' if value.present? && !value.end_with?('/')
value
end end
def url=(value) def url=(value)
value += '/' if value.present? && !value.end_with?('/') write_with_ending_slash(:url, value)
write_attribute(:url, value)
@uri = nil @uri = nil
end end
def alternate_url
read_with_ending_slash(:alternate_url)
end
def alternate_url=(value)
write_with_ending_slash(:alternate_url, value)
@alternate_uri = nil
end
def uri def uri
@uri ||= URI.parse(url) if url.present? @uri ||= URI.parse(url) if url.present?
end end
def alternate_uri
@alternate_uri ||= URI.parse(alternate_url) if alternate_url.present?
end
def geo_transfers_url(file_type, file_id) def geo_transfers_url(file_type, file_id)
geo_api_url("transfers/#{file_type}/#{file_id}") geo_api_url("transfers/#{file_type}/#{file_id}")
end end
...@@ -158,6 +168,12 @@ class GeoNode < ActiveRecord::Base ...@@ -158,6 +168,12 @@ class GeoNode < ActiveRecord::Base
Gitlab::Routing.url_helpers.oauth_geo_callback_url(url_helper_args) Gitlab::Routing.url_helpers.oauth_geo_callback_url(url_helper_args)
end end
def alternate_oauth_callback_url
return unless alternate_url.present?
Gitlab::Routing.url_helpers.oauth_geo_callback_url(alternate_url_helper_args)
end
def oauth_logout_url(state) def oauth_logout_url(state)
Gitlab::Routing.url_helpers.oauth_geo_logout_url(url_helper_args.merge(state: state)) Gitlab::Routing.url_helpers.oauth_geo_logout_url(url_helper_args.merge(state: state))
end end
...@@ -257,7 +273,15 @@ class GeoNode < ActiveRecord::Base ...@@ -257,7 +273,15 @@ class GeoNode < ActiveRecord::Base
end end
def url_helper_args def url_helper_args
{ protocol: uri.scheme, host: uri.host, port: uri.port, script_name: uri.path } url_helper_options(uri)
end
def alternate_url_helper_args
url_helper_options(alternate_uri)
end
def url_helper_options(given_uri)
{ protocol: given_uri.scheme, host: given_uri.host, port: given_uri.port, script_name: given_uri.path }
end end
def update_dependents_attributes def update_dependents_attributes
...@@ -276,12 +300,22 @@ class GeoNode < ActiveRecord::Base ...@@ -276,12 +300,22 @@ class GeoNode < ActiveRecord::Base
end end
end end
def check_url_is_valid def url_is_http
if uri.present? && !%w[http https].include?(uri.scheme) url_is_http_for(:url, uri)
errors.add(:url, 'scheme must be http or https') end
def alternate_url_is_http
url_is_http_for(:alternate_url, alternate_uri)
end
def url_is_http_for(attribute, uri_value)
return unless uri_value
unless %w[http https].include?(uri_value.scheme)
errors.add(attribute, 'scheme must be http or https')
end end
rescue URI::InvalidURIError rescue URI::InvalidURIError
errors.add(:url, 'is invalid') errors.add(attribute, 'is not a valid URI')
end end
def update_clone_url def update_clone_url
...@@ -291,10 +325,29 @@ class GeoNode < ActiveRecord::Base ...@@ -291,10 +325,29 @@ class GeoNode < ActiveRecord::Base
def update_oauth_application! def update_oauth_application!
self.build_oauth_application if oauth_application.nil? self.build_oauth_application if oauth_application.nil?
self.oauth_application.name = "Geo node: #{self.url}" self.oauth_application.name = "Geo node: #{self.url}"
self.oauth_application.redirect_uri = oauth_callback_url self.oauth_application.redirect_uri = [oauth_callback_url, alternate_oauth_callback_url].compact.join("\n")
end end
def expire_cache! def expire_cache!
Gitlab::Geo.expire_cache! Gitlab::Geo.expire_cache!
end end
def read_with_ending_slash(attribute)
value = read_attribute(attribute)
add_ending_slash(value)
end
def write_with_ending_slash(attribute, value)
value = add_ending_slash(value)
write_attribute(attribute, value)
end
def add_ending_slash(value)
return value if value.blank?
return value if value.end_with?('/')
"#{value}/"
end
end end
= form_errors(geo_node) = form_errors(geo_node)
.form-group.row .form-row.form-group
.col-sm-2 .form-group.col-sm-6
= form.label :url, 'URL', class: 'col-form-label' = form.label :url, s_('Geo|URL'), class: 'font-weight-bold'
.col-sm-10
= form.text_field :url, class: 'form-control' = form.text_field :url, class: 'form-control'
.form-group.row .form-group.col-sm-6.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.offset-sm-2.col-sm-10 = form.label :alternate_url, s_('Geo|Alternate URL'), class: 'font-weight-bold'
= form.text_field :alternate_url, class: 'form-control'
.form-text.text-muted= s_('Geo|To support OAuth logins to this node at a different domain than URL')
.form-row.form-group
.col-sm-12
.form-check .form-check
= form.check_box :primary, class: 'form-check-input' = form.check_box :primary, class: 'form-check-input'
= form.label :primary, class: 'form-check-label' do = form.label :primary, class: 'form-check-label' do
%strong This is a primary node %span= s_('Geo|This is a primary node')
.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?) }
.form-group.row .col-sm-4
.col-sm-2 = form.label :selective_sync_type, s_('Geo|Selective synchronization'), class: 'font-weight-bold'
= form.label :selective_sync_type, s_('Selective synchronization'), class: 'col-form-label' = form.select :selective_sync_type, selective_sync_type_options_for_select(geo_node), {}, { class: "form-control js-geo-node-selective-sync-type" }
.col-sm-10
= form.select :selective_sync_type, selective_sync_type_options_for_select(geo_node), .form-row.form-group.js-sync-by-namespace{ class: ('hidden' unless geo_node.selective_sync_by_namespaces?) }
{}, { class: "form-control js-geo-node-selective-sync-type" } .col-sm-4
= form.label :namespace_ids, s_('Geo|Groups to synchronize'), class: 'font-weight-bold'
.form-group.row.js-sync-by-namespace{ class: ('hidden' unless geo_node.selective_sync_by_namespaces?) } = hidden_field_tag "#{form.object_name}[namespace_ids]", geo_node.namespace_ids.join(","), class: 'js-geo-node-namespaces', data: { selected: node_namespaces_options(geo_node.namespaces).to_json }
.col-sm-2 .form-text.text-muted= s_('Geo|Choose which groups you wish to synchronize to this secondary node.')
= form.label :namespace_ids, s_('Geo|Groups to synchronize'), class: 'col-form-label'
.col-sm-10 .form-row.form-group.js-sync-by-shard{ class: ('hidden' unless geo_node.selective_sync_by_shards?) }
= hidden_field_tag "#{form.object_name}[namespace_ids]", geo_node.namespace_ids.join(","), class: 'js-geo-node-namespaces', data: { selected: node_namespaces_options(geo_node.namespaces).to_json } .col-sm-4
.form-text.text-muted = form.label :selective_sync_shards, s_('Geo|Shards to synchronize'), class: 'font-weight-bold'
#{ s_("Choose which groups you wish to synchronize to this secondary node.") } = form.select :selective_sync_shards, repository_storages_options_for_select(geo_node.selective_sync_shards), { include_hidden: false }, multiple: true, class: 'form-control'
.form-text.text-muted= s_('Choose which shards you wish to synchronize to this secondary node.')
.form-group.row.js-sync-by-shard{ class: ('hidden' unless geo_node.selective_sync_by_shards?) }
.col-sm-2 .form-row.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
= form.label :selective_sync_shards, s_('Geo|Shards to synchronize'), class: 'col-form-label' .col-sm-8
.col-sm-10 = form.label :repos_max_capacity, s_('Geo|Repository sync capacity'), class: 'font-weight-bold'
= form.select :selective_sync_shards, repository_storages_options_for_select(geo_node.selective_sync_shards), = form.number_field :repos_max_capacity, class: 'form-control col-sm-2', min: 0
{ include_hidden: false }, multiple: true, class: 'form-control' .form-text.text-muted= s_('Control the maximum concurrency of repository backfill for this secondary node')
.form-text.text-muted
#{ s_("Choose which shards you wish to synchronize to this secondary node.") } .form-row.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-8
.form-group.row.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) } = form.label :files_max_capacity, s_('Geo|File sync capacity'), class: 'font-weight-bold'
.col-sm-2 = form.number_field :files_max_capacity, class: 'form-control col-sm-2', min: 0
= form.label :repos_max_capacity, s_('Geo|Repository sync capacity'), class: 'col-form-label' .form-text.text-muted= s_('Geo|Control the maximum concurrency of LFS/attachment backfill for this secondary node')
.col-sm-10
= form.number_field :repos_max_capacity, class: 'form-control', min: 0 .form-row.form-group
.form-text.text-muted .col-sm-8
#{ s_('Control the maximum concurrency of repository backfill for this secondary node') } = form.label :verification_max_capacity, s_('Geo|Verification capacity'), class: 'font-weight-bold'
= form.number_field :verification_max_capacity, class: 'form-control col-sm-2', min: 0
.form-group.row.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) } .form-text.text-muted= s_('Geo|Control the maximum concurrency of verification operations for this Geo node')
.col-sm-2
= form.label :files_max_capacity, s_('Geo|File sync capacity'), class: 'col-form-label' .form-row.form-group.js-hide-if-geo-secondary{ class: ('hidden' unless geo_node.primary?) }
.col-sm-10 .col-sm-8
= form.number_field :files_max_capacity, class: 'form-control', min: 0 = form.label :minimum_reverification_interval, s_('Geo|Re-verification interval'), class: 'font-weight-bold'
.form-text.text-muted = form.number_field :minimum_reverification_interval, class: 'form-control col-sm-2', min: 1
#{ s_('Control the maximum concurrency of LFS/attachment backfill for this secondary node') } .form-text.text-muted= s_('Geo|Control the minimum interval in days that a repository should be reverified for this primary node')
.form-group.row
.col-sm-2
= form.label :verification_max_capacity, s_('Geo|Verification capacity'), class: 'col-form-label'
.col-sm-10
= form.number_field :verification_max_capacity, class: 'form-control', min: 0
.form-text.text-muted
#{ s_('Control the maximum concurrency of verification operations for this Geo node') }
.form-group.row.js-hide-if-geo-secondary{ class: ('hidden' unless geo_node.primary?) }
.col-sm-2
= form.label :minimum_reverification_interval, s_('Geo|Re-verification interval'), class: 'col-form-label'
.col-sm-10
= form.number_field :minimum_reverification_interval, class: 'form-control', min: 1
.form-text.text-muted
#{ s_('Control the minimum interval in days that a repository should be reverified for this primary node') }
---
title: 'Geo: Allow OAuth login to a secondary node at an alternate URL'
merge_request: 9544
author:
type: added
# frozen_string_literal: true
class AddAlternateUrlToGeoNodes < ActiveRecord::Migration[5.0]
def change
add_column :geo_nodes, :alternate_url, :string
end
end
...@@ -144,6 +144,7 @@ module API ...@@ -144,6 +144,7 @@ module API
params do params do
optional :enabled, type: Boolean, desc: 'Flag indicating if the Geo node is enabled' optional :enabled, type: Boolean, desc: 'Flag indicating if the Geo node is enabled'
optional :url, type: String, desc: 'The URL to connect to the Geo node' optional :url, type: String, desc: 'The URL to connect to the Geo node'
optional :alternate_url, type: String, desc: 'An alternate URL to allow OAuth logins to this secondary node'
optional :files_max_capacity, type: Integer, desc: 'Control the maximum concurrency of LFS/attachment backfill for this secondary node' optional :files_max_capacity, type: Integer, desc: 'Control the maximum concurrency of LFS/attachment backfill for this secondary node'
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'
......
...@@ -434,6 +434,7 @@ module EE ...@@ -434,6 +434,7 @@ module EE
expose :id expose :id
expose :url expose :url
expose :alternate_url
expose :primary?, as: :primary expose :primary?, as: :primary
expose :enabled expose :enabled
expose :current?, as: :current expose :current?, as: :current
......
...@@ -110,7 +110,7 @@ describe Admin::Geo::NodesController, :postgresql do ...@@ -110,7 +110,7 @@ describe Admin::Geo::NodesController, :postgresql do
end end
describe '#update' do describe '#update' do
let(:geo_node_attributes) { { url: 'http://example.com', selective_sync_shards: %w[foo bar] } } let(:geo_node_attributes) { { url: 'http://example.com', alternate_url: 'http://anotherexample.com', selective_sync_shards: %w[foo bar] } }
let(:geo_node) { create(:geo_node) } let(:geo_node) { create(:geo_node) }
...@@ -137,6 +137,7 @@ describe Admin::Geo::NodesController, :postgresql do ...@@ -137,6 +137,7 @@ describe Admin::Geo::NodesController, :postgresql do
geo_node.reload geo_node.reload
expect(geo_node.url.chomp('/')).to eq(geo_node_attributes[:url]) expect(geo_node.url.chomp('/')).to eq(geo_node_attributes[:url])
expect(geo_node.alternate_url.chomp('/')).to eq(geo_node_attributes[:alternate_url])
expect(geo_node.selective_sync_shards).to eq(%w[foo bar]) expect(geo_node.selective_sync_shards).to eq(%w[foo bar])
end end
......
...@@ -135,6 +135,20 @@ describe 'admin Geo Nodes', :js do ...@@ -135,6 +135,20 @@ describe 'admin Geo Nodes', :js do
expect(page).to have_content('Primary') expect(page).to have_content('Primary')
end end
end end
it "allows the admin to update a secondary node's alternate URL" do
fill_in 'Alternate URL', with: 'http://someloadbalancer.com'
click_button 'Save changes'
expect(current_path).to eq admin_geo_nodes_path
wait_for_requests
page.within(find('.geo-node-item', match: :first)) do
click_button 'Other information'
expect(page).to have_content('http://someloadbalancer.com')
end
end
end end
describe 'remove an existing Geo Node' do describe 'remove an existing Geo Node' do
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
"required" : [ "required" : [
"id", "id",
"url", "url",
"alternate_url",
"primary", "primary",
"enabled", "enabled",
"current", "current",
...@@ -15,6 +16,7 @@ ...@@ -15,6 +16,7 @@
"properties" : { "properties" : {
"id": { "type": "integer" }, "id": { "type": "integer" },
"url": { "type": ["string", "null"] }, "url": { "type": ["string", "null"] },
"alternate_url": { "type": ["string", "null"] },
"primary": { "type": "boolean" }, "primary": { "type": "boolean" },
"enabled": { "type": "boolean" }, "enabled": { "type": "boolean" },
"current": { "type": "boolean" }, "current": { "type": "boolean" },
......
...@@ -3,15 +3,17 @@ import Vue from 'vue'; ...@@ -3,15 +3,17 @@ import Vue from 'vue';
import NodeDetailsSectionOtherComponent from 'ee/geo_nodes/components/node_detail_sections/node_details_section_other.vue'; import NodeDetailsSectionOtherComponent from 'ee/geo_nodes/components/node_detail_sections/node_details_section_other.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import { mockNodeDetails } from 'ee_spec/geo_nodes/mock_data'; import { mockNode, mockNodeDetails } from 'ee_spec/geo_nodes/mock_data';
const createComponent = ( const createComponent = (
node = Object.assign({}, mockNode),
nodeDetails = Object.assign({}, mockNodeDetails), nodeDetails = Object.assign({}, mockNodeDetails),
nodeTypePrimary = false, nodeTypePrimary = false,
) => { ) => {
const Component = Vue.extend(NodeDetailsSectionOtherComponent); const Component = Vue.extend(NodeDetailsSectionOtherComponent);
return mountComponent(Component, { return mountComponent(Component, {
node,
nodeDetails, nodeDetails,
nodeTypePrimary, nodeTypePrimary,
}); });
...@@ -37,7 +39,7 @@ describe('NodeDetailsSectionOther', () => { ...@@ -37,7 +39,7 @@ describe('NodeDetailsSectionOther', () => {
describe('computed', () => { describe('computed', () => {
describe('nodeDetailItems', () => { describe('nodeDetailItems', () => {
it('returns array containing items to show under primary node when prop `nodeTypePrimary` is true', () => { it('returns array containing items to show under primary node when prop `nodeTypePrimary` is true', () => {
const vmNodePrimary = createComponent(mockNodeDetails, true); const vmNodePrimary = createComponent(mockNode, mockNodeDetails, true);
const items = vmNodePrimary.nodeDetailItems; const items = vmNodePrimary.nodeDetailItems;
...@@ -53,7 +55,7 @@ describe('NodeDetailsSectionOther', () => { ...@@ -53,7 +55,7 @@ describe('NodeDetailsSectionOther', () => {
it('returns array containing items to show under secondary node when prop `nodeTypePrimary` is false', () => { it('returns array containing items to show under secondary node when prop `nodeTypePrimary` is false', () => {
const items = vm.nodeDetailItems; const items = vm.nodeDetailItems;
expect(items.length).toBe(1); expect(items.length).toBe(2);
expect(items[0].itemTitle).toBe('Storage config'); expect(items[0].itemTitle).toBe('Storage config');
}); });
}); });
......
...@@ -25,6 +25,7 @@ export const mockNodes = [ ...@@ -25,6 +25,7 @@ export const mockNodes = [
{ {
id: 2, id: 2,
url: 'http://127.0.0.1:3002/', url: 'http://127.0.0.1:3002/',
alternate_url: 'http://example.com:3002/',
primary: false, primary: false,
enabled: true, enabled: true,
current: false, current: false,
...@@ -43,6 +44,7 @@ export const mockNodes = [ ...@@ -43,6 +44,7 @@ export const mockNodes = [
export const mockNode = { export const mockNode = {
id: 1, id: 1,
url: 'http://127.0.0.1:3001/', url: 'http://127.0.0.1:3001/',
alternateUrl: '',
primary: true, primary: true,
current: true, current: true,
enabled: true, enabled: true,
......
This diff is collapsed.
...@@ -212,6 +212,7 @@ describe API::GeoNodes, :geo, :prometheus, api: true do ...@@ -212,6 +212,7 @@ describe API::GeoNodes, :geo, :prometheus, api: true do
params = { params = {
enabled: false, enabled: false,
url: 'https://updated.example.com/', url: 'https://updated.example.com/',
alternate_url: 'https://alternate.example.com/',
files_max_capacity: 33, files_max_capacity: 33,
repos_max_capacity: 44, repos_max_capacity: 44,
verification_max_capacity: 55 verification_max_capacity: 55
......
...@@ -1923,9 +1923,6 @@ msgstr "" ...@@ -1923,9 +1923,6 @@ msgstr ""
msgid "Choose what content you want to see on a group’s overview page" msgid "Choose what content you want to see on a group’s overview page"
msgstr "" msgstr ""
msgid "Choose which groups you wish to synchronize to this secondary node."
msgstr ""
msgid "Choose which repositories you want to connect and run CI/CD pipelines." msgid "Choose which repositories you want to connect and run CI/CD pipelines."
msgstr "" msgstr ""
...@@ -2888,18 +2885,9 @@ msgstr "" ...@@ -2888,18 +2885,9 @@ msgstr ""
msgid "Control the display of third party offers." msgid "Control the display of third party offers."
msgstr "" msgstr ""
msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
msgid "Control the maximum concurrency of repository backfill for this secondary node" msgid "Control the maximum concurrency of repository backfill for this secondary node"
msgstr "" msgstr ""
msgid "Control the maximum concurrency of verification operations for this Geo node"
msgstr ""
msgid "Control the minimum interval in days that a repository should be reverified for this primary node"
msgstr ""
msgid "ConvDev Index" msgid "ConvDev Index"
msgstr "" msgstr ""
...@@ -4572,6 +4560,9 @@ msgstr "" ...@@ -4572,6 +4560,9 @@ msgstr ""
msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage." msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
msgstr "" msgstr ""
msgid "GeoNodes|Alternate URL"
msgstr ""
msgid "GeoNodes|Checksummed" msgid "GeoNodes|Checksummed"
msgstr "" msgstr ""
...@@ -4752,9 +4743,24 @@ msgstr "" ...@@ -4752,9 +4743,24 @@ msgstr ""
msgid "Geo|All projects are being scheduled for re-sync" msgid "Geo|All projects are being scheduled for re-sync"
msgstr "" msgstr ""
msgid "Geo|Alternate URL"
msgstr ""
msgid "Geo|Batch operations" msgid "Geo|Batch operations"
msgstr "" msgstr ""
msgid "Geo|Choose which groups you wish to synchronize to this secondary node."
msgstr ""
msgid "Geo|Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
msgid "Geo|Control the maximum concurrency of verification operations for this Geo node"
msgstr ""
msgid "Geo|Control the minimum interval in days that a repository should be reverified for this primary node"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project." msgid "Geo|Could not remove tracking entry for an existing project."
msgstr "" msgstr ""
...@@ -4842,6 +4848,9 @@ msgstr "" ...@@ -4842,6 +4848,9 @@ msgstr ""
msgid "Geo|Select groups to replicate." msgid "Geo|Select groups to replicate."
msgstr "" msgstr ""
msgid "Geo|Selective synchronization"
msgstr ""
msgid "Geo|Shards to synchronize" msgid "Geo|Shards to synchronize"
msgstr "" msgstr ""
...@@ -4854,12 +4863,21 @@ msgstr "" ...@@ -4854,12 +4863,21 @@ msgstr ""
msgid "Geo|Synchronization failed - %{error}" msgid "Geo|Synchronization failed - %{error}"
msgstr "" msgstr ""
msgid "Geo|This is a primary node"
msgstr ""
msgid "Geo|To support OAuth logins to this node at a different domain than URL"
msgstr ""
msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed." msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed."
msgstr "" msgstr ""
msgid "Geo|Tracking entry will be removed. Are you sure?" msgid "Geo|Tracking entry will be removed. Are you sure?"
msgstr "" msgstr ""
msgid "Geo|URL"
msgstr ""
msgid "Geo|Unknown state" msgid "Geo|Unknown state"
msgstr "" msgstr ""
...@@ -8900,9 +8918,6 @@ msgstr "" ...@@ -8900,9 +8918,6 @@ msgstr ""
msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user." msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user."
msgstr "" msgstr ""
msgid "Selective synchronization"
msgstr ""
msgid "Send email" msgid "Send email"
msgstr "" msgstr ""
......
...@@ -7,7 +7,7 @@ module QA ...@@ -7,7 +7,7 @@ module QA
class New < QA::Page::Base class New < QA::Page::Base
view 'ee/app/views/admin/geo/nodes/_form.html.haml' do view 'ee/app/views/admin/geo/nodes/_form.html.haml' do
element :node_url_field, 'text_field :url' # rubocop:disable QA/ElementWithPattern element :node_url_field, 'text_field :url' # rubocop:disable QA/ElementWithPattern
element :node_url_placeholder, "label :url, 'URL'" # rubocop:disable QA/ElementWithPattern element :node_url_placeholder, "label :url" # rubocop:disable QA/ElementWithPattern
end end
view 'ee/app/views/admin/geo/nodes/new.html.haml' do view 'ee/app/views/admin/geo/nodes/new.html.haml' do
......
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