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
t.text "selective_sync_shards"
t.integer "verification_max_capacity", default: 100, 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 ["primary"], name: "index_geo_nodes_on_primary", using: :btree
t.index ["url"], name: "index_geo_nodes_on_url", unique: true, using: :btree
......
......@@ -31,6 +31,7 @@ Example response:
{
"id": 2,
"url": "https://secondary.example.com/",
"alternate_url": "https://alternate.example.com/",
"primary": false,
"enabled": true,
"current": false,
......@@ -83,6 +84,7 @@ PUT /geo_nodes/:id
| `id` | integer | yes | The ID of the Geo node. |
| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. |
| `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. |
| `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. |
......@@ -93,6 +95,7 @@ Example response:
{
"id": 1,
"url": "https://secondary.example.com/",
"alternate_url": "https://alternate.example.com/",
"primary": false,
"enabled": true,
"current": true,
......
......@@ -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. |
| 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. |
| Alternate URL | Allows users to log in to the secondary at an alternate URL (required for OAuth) |
## Geo backfill
......@@ -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
under heavy load and backfill is reducing its availability for normal requests,
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 {
</script>
<template>
<div class="node-detail-item prepend-top-15 prepend-left-10">
<div class="node-detail-item">
<div class="node-detail-title">
<span> {{ itemTitle }} </span>
<icon
......
......@@ -74,7 +74,11 @@ export default {
:node-details="nodeDetails"
: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">
<p class="node-health-message">{{ errorMessage }}</p>
</div>
......
......@@ -17,6 +17,10 @@ export default {
},
mixins: [DetailsSectionMixin],
props: {
node: {
type: Object,
required: true,
},
nodeDetails: {
type: Object,
required: true,
......@@ -65,6 +69,12 @@ export default {
itemValueType: VALUE_TYPE.PLAIN,
cssClass: this.storageShardsCssClass,
},
{
itemTitle: s__('GeoNodes|Alternate URL'),
itemValue: this.node.alternateUrl,
itemValueType: VALUE_TYPE.PLAIN,
cssClass: 'node-detail-value-bold',
},
];
},
storageShardsStatus() {
......@@ -81,6 +91,14 @@ export default {
? `${cssClass} node-detail-value-error`
: 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: {
handleSectionToggle(toggleState) {
......@@ -100,15 +118,19 @@ export default {
</div>
<div
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
:item-title="s__('GeoNodes|Storage config')"
:item-value="storageShardsStatus"
:item-value-type="$options.valueType.PLAIN"
v-for="(nodeDetailItem, index) in nodeDetailItems"
:key="index"
: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-tooltip="statusInfoStaleMessage"
:css-class="storageShardsCssClass"
/>
</div>
</div>
......
......@@ -141,6 +141,7 @@ export default {
:item-value-stale-tooltip="statusInfoStaleMessage"
:custom-type="nodeDetailItem.customType"
:event-type-log-status="nodeDetailItem.eventTypeLogStatus"
class="prepend-top-15 prepend-left-10"
/>
</div>
</div>
......
......@@ -136,6 +136,7 @@ export default {
:failure-label="nodeDetailItem.failureLabel"
:custom-type="nodeDetailItem.customType"
:help-info="nodeDetailItem.helpInfo"
class="prepend-top-15 prepend-left-10"
/>
</div>
</template>
......
......@@ -48,6 +48,7 @@ export default class GeoNodesStore {
primary,
current,
enabled,
alternateUrl: rawNode.alternate_url || '',
nodeActionActive: false,
basePath: rawNode._links.self,
repairPath: rawNode._links.repair,
......
......@@ -50,6 +50,7 @@ class Admin::Geo::NodesController < Admin::ApplicationController
def geo_node_params
params.require(:geo_node).permit(
:url,
:alternate_url,
:primary,
:selective_sync_type,
:namespace_ids,
......
......@@ -18,7 +18,8 @@ class GeoNode < ActiveRecord::Base
primary: 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 :enabled, if: :primary, acceptance: { message: 'Geo primary node cannot be disabled' }
......@@ -121,24 +122,33 @@ class GeoNode < ActiveRecord::Base
end
def url
value = read_attribute(:url)
value += '/' if value.present? && !value.end_with?('/')
value
read_with_ending_slash(:url)
end
def url=(value)
value += '/' if value.present? && !value.end_with?('/')
write_attribute(:url, value)
write_with_ending_slash(:url, value)
@uri = nil
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
@uri ||= URI.parse(url) if url.present?
end
def alternate_uri
@alternate_uri ||= URI.parse(alternate_url) if alternate_url.present?
end
def geo_transfers_url(file_type, file_id)
geo_api_url("transfers/#{file_type}/#{file_id}")
end
......@@ -158,6 +168,12 @@ class GeoNode < ActiveRecord::Base
Gitlab::Routing.url_helpers.oauth_geo_callback_url(url_helper_args)
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)
Gitlab::Routing.url_helpers.oauth_geo_logout_url(url_helper_args.merge(state: state))
end
......@@ -257,7 +273,15 @@ class GeoNode < ActiveRecord::Base
end
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
def update_dependents_attributes
......@@ -276,12 +300,22 @@ class GeoNode < ActiveRecord::Base
end
end
def check_url_is_valid
if uri.present? && !%w[http https].include?(uri.scheme)
errors.add(:url, 'scheme must be http or https')
def url_is_http
url_is_http_for(:url, uri)
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
rescue URI::InvalidURIError
errors.add(:url, 'is invalid')
errors.add(attribute, 'is not a valid URI')
end
def update_clone_url
......@@ -291,10 +325,29 @@ class GeoNode < ActiveRecord::Base
def update_oauth_application!
self.build_oauth_application if oauth_application.nil?
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
def expire_cache!
Gitlab::Geo.expire_cache!
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
= form_errors(geo_node)
.form-group.row
.col-sm-2
= form.label :url, 'URL', class: 'col-form-label'
.col-sm-10
.form-row.form-group
.form-group.col-sm-6
= form.label :url, s_('Geo|URL'), class: 'font-weight-bold'
= form.text_field :url, class: 'form-control'
.form-group.row
.offset-sm-2.col-sm-10
.form-group.col-sm-6.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
= 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_box :primary, class: 'form-check-input'
= form.label :primary, class: 'form-check-label' do
%strong This is a primary node
.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.form-group.row
.col-sm-2
= form.label :selective_sync_type, s_('Selective synchronization'), class: 'col-form-label'
.col-sm-10
= form.select :selective_sync_type, selective_sync_type_options_for_select(geo_node),
{}, { class: "form-control js-geo-node-selective-sync-type" }
.form-group.row.js-sync-by-namespace{ class: ('hidden' unless geo_node.selective_sync_by_namespaces?) }
.col-sm-2
= form.label :namespace_ids, s_('Geo|Groups to synchronize'), class: 'col-form-label'
.col-sm-10
= 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 }
.form-text.text-muted
#{ s_("Choose which groups 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.label :selective_sync_shards, s_('Geo|Shards to synchronize'), class: 'col-form-label'
.col-sm-10
= 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-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-2
= form.label :repos_max_capacity, s_('Geo|Repository sync capacity'), class: 'col-form-label'
.col-sm-10
= form.number_field :repos_max_capacity, class: 'form-control', min: 0
.form-text.text-muted
#{ s_('Control the maximum concurrency of repository backfill for this secondary node') }
.form-group.row.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-2
= form.label :files_max_capacity, s_('Geo|File sync capacity'), class: 'col-form-label'
.col-sm-10
= form.number_field :files_max_capacity, class: 'form-control', min: 0
.form-text.text-muted
#{ s_('Control the maximum concurrency of LFS/attachment backfill for this secondary 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') }
%span= s_('Geo|This is a primary node')
.form-row.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-4
= form.label :selective_sync_type, s_('Geo|Selective synchronization'), class: 'font-weight-bold'
= form.select :selective_sync_type, selective_sync_type_options_for_select(geo_node), {}, { class: "form-control js-geo-node-selective-sync-type" }
.form-row.form-group.js-sync-by-namespace{ class: ('hidden' unless geo_node.selective_sync_by_namespaces?) }
.col-sm-4
= form.label :namespace_ids, s_('Geo|Groups to synchronize'), class: 'font-weight-bold'
= 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 }
.form-text.text-muted= s_('Geo|Choose which groups you wish to synchronize to this secondary node.')
.form-row.form-group.js-sync-by-shard{ class: ('hidden' unless geo_node.selective_sync_by_shards?) }
.col-sm-4
= form.label :selective_sync_shards, s_('Geo|Shards to synchronize'), class: 'font-weight-bold'
= 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-row.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-8
= form.label :repos_max_capacity, s_('Geo|Repository sync capacity'), class: 'font-weight-bold'
= form.number_field :repos_max_capacity, class: 'form-control col-sm-2', min: 0
.form-text.text-muted= s_('Control the maximum concurrency of repository backfill for this secondary node')
.form-row.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-8
= form.label :files_max_capacity, s_('Geo|File sync capacity'), class: 'font-weight-bold'
= form.number_field :files_max_capacity, class: 'form-control col-sm-2', min: 0
.form-text.text-muted= s_('Geo|Control the maximum concurrency of LFS/attachment backfill for this secondary node')
.form-row.form-group
.col-sm-8
= 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-text.text-muted= s_('Geo|Control the maximum concurrency of verification operations for this Geo node')
.form-row.form-group.js-hide-if-geo-secondary{ class: ('hidden' unless geo_node.primary?) }
.col-sm-8
= 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-text.text-muted= s_('Geo|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
params do
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 :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 :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'
......
......@@ -434,6 +434,7 @@ module EE
expose :id
expose :url
expose :alternate_url
expose :primary?, as: :primary
expose :enabled
expose :current?, as: :current
......
......@@ -110,7 +110,7 @@ describe Admin::Geo::NodesController, :postgresql do
end
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) }
......@@ -137,6 +137,7 @@ describe Admin::Geo::NodesController, :postgresql do
geo_node.reload
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])
end
......
......@@ -135,6 +135,20 @@ describe 'admin Geo Nodes', :js do
expect(page).to have_content('Primary')
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
describe 'remove an existing Geo Node' do
......
......@@ -3,6 +3,7 @@
"required" : [
"id",
"url",
"alternate_url",
"primary",
"enabled",
"current",
......@@ -15,6 +16,7 @@
"properties" : {
"id": { "type": "integer" },
"url": { "type": ["string", "null"] },
"alternate_url": { "type": ["string", "null"] },
"primary": { "type": "boolean" },
"enabled": { "type": "boolean" },
"current": { "type": "boolean" },
......
......@@ -3,15 +3,17 @@ import Vue from '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 { 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 = (
node = Object.assign({}, mockNode),
nodeDetails = Object.assign({}, mockNodeDetails),
nodeTypePrimary = false,
) => {
const Component = Vue.extend(NodeDetailsSectionOtherComponent);
return mountComponent(Component, {
node,
nodeDetails,
nodeTypePrimary,
});
......@@ -37,7 +39,7 @@ describe('NodeDetailsSectionOther', () => {
describe('computed', () => {
describe('nodeDetailItems', () => {
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;
......@@ -53,7 +55,7 @@ describe('NodeDetailsSectionOther', () => {
it('returns array containing items to show under secondary node when prop `nodeTypePrimary` is false', () => {
const items = vm.nodeDetailItems;
expect(items.length).toBe(1);
expect(items.length).toBe(2);
expect(items[0].itemTitle).toBe('Storage config');
});
});
......
......@@ -25,6 +25,7 @@ export const mockNodes = [
{
id: 2,
url: 'http://127.0.0.1:3002/',
alternate_url: 'http://example.com:3002/',
primary: false,
enabled: true,
current: false,
......@@ -43,6 +44,7 @@ export const mockNodes = [
export const mockNode = {
id: 1,
url: 'http://127.0.0.1:3001/',
alternateUrl: '',
primary: true,
current: true,
enabled: true,
......
......@@ -4,13 +4,14 @@ describe GeoNode, type: :model do
using RSpec::Parameterized::TableSyntax
include ::EE::GeoHelpers
let(:new_node) { create(:geo_node, url: 'https://localhost:3000/gitlab') }
let(:new_primary_node) { create(:geo_node, :primary, url: 'https://localhost:3000/gitlab') }
let(:dummy_url) { 'https://localhost:3000/gitlab' }
let(:new_node_attrs) { { url: dummy_url } }
let(:new_node) { create(:geo_node, new_node_attrs) }
let(:new_primary_node) { create(:geo_node, :primary, new_node_attrs) }
let(:empty_node) { described_class.new }
let(:primary_node) { create(:geo_node, :primary) }
let(:node) { create(:geo_node) }
let(:dummy_url) { 'https://localhost:3000/gitlab' }
let(:url_helpers) { Gitlab::Routing.url_helpers }
let(:api_version) { API::API.version }
......@@ -28,7 +29,7 @@ describe GeoNode, type: :model do
it { is_expected.to validate_numericality_of(:verification_max_capacity).is_greater_than_or_equal_to(0) }
it { is_expected.to validate_numericality_of(:minimum_reverification_interval).is_greater_than_or_equal_to(1) }
context 'primary node' do
context 'when validating primary node' do
it 'cannot be disabled' do
primary_node.enabled = false
......@@ -36,6 +37,50 @@ describe GeoNode, type: :model do
expect(primary_node.errors).to include(:enabled)
end
end
context 'when validating url' do
subject { build(:geo_node, url: url) }
context 'when url is http' do
let(:url) { 'http://foo' }
it { is_expected.to be_valid }
end
context 'when url is https' do
let(:url) { 'https://foo' }
it { is_expected.to be_valid }
end
context 'when url is not http or https' do
let(:url) { 'nothttp://foo' }
it { is_expected.not_to be_valid }
end
end
context 'when validating alternate_url' do
subject { build(:geo_node, alternate_url: alternate_url) }
context 'when alternate_url is http' do
let(:alternate_url) { 'http://foo' }
it { is_expected.to be_valid }
end
context 'when alternate_url is https' do
let(:alternate_url) { 'https://foo' }
it { is_expected.to be_valid }
end
context 'when alternate_url is not http or https' do
let(:alternate_url) { 'nothttp://foo' }
it { is_expected.not_to be_valid }
end
end
end
context 'default values' do
......@@ -62,18 +107,92 @@ describe GeoNode, type: :model do
end
context 'dependent models and attributes for GeoNode' do
context 'on create' do
it 'saves a corresponding oauth application if it is a secondary node' do
expect(node.oauth_application).to be_persisted
context 'when validating' do
context 'when it is a secondary node' do
before do
node
end
context 'when the oauth_application is missing' do
before do
node.oauth_application.destroy
node.oauth_application = nil
end
it 'builds an oauth_application' do
expect(node).to be_valid
expect(node.oauth_application).to be_present
expect(node.oauth_application.redirect_uri).to eq(node.oauth_callback_url)
end
end
it 'overwrites redirect_uri' do
node.oauth_application.redirect_uri = 'http://wrong-callback-url'
node.oauth_application.save!
expect(node).to be_valid
expect(node.oauth_application.redirect_uri).to eq(node.oauth_callback_url)
end
context 'when the node has an alternate_url' do
it 'adds an alternate callback URL' do
node.alternate_url = 'http://alternate-url.com:1234/gitlab/'
expect(node).to be_valid
expected = [node.oauth_callback_url, node.alternate_oauth_callback_url].join("\n")
expect(node.oauth_application.redirect_uri).to eq(expected)
end
end
end
context 'when is a primary node' do
it 'has no oauth_application' do
expect(primary_node.oauth_application).not_to be_present
context 'when it is a primary node' do
before do
primary_node
end
context 'when it does not have an oauth_application' do
it 'does not create an oauth_application' do
primary_node.oauth_application = nil
expect(primary_node).to be_valid
expect(primary_node.oauth_application).to be_nil
end
end
context 'when it has an oauth_application' do
# TODO Should it instead be destroyed?
# https://gitlab.com/gitlab-org/gitlab-ee/issues/10225
it 'disassociates the oauth_application' do
primary_node.oauth_application = create(:oauth_application)
expect(primary_node).to be_valid
expect(primary_node.oauth_application).to be_nil
end
end
context 'when clone_url_prefix is nil' do
it 'sets current clone_url_prefix' do
primary_node.clone_url_prefix = nil
expect(primary_node).to be_valid
expect(primary_node.clone_url_prefix).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix)
end
end
it 'persists current clone_url_prefix' do
expect(primary_node.clone_url_prefix).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix)
context 'when clone_url_prefix has changed' do
it 'sets current clone_url_prefix' do
primary_node.clone_url_prefix = 'foo'
expect(primary_node).to be_valid
expect(primary_node.clone_url_prefix).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix)
end
end
end
end
......@@ -245,18 +364,13 @@ describe GeoNode, type: :model do
stub_config_setting(port: 443)
stub_config_setting(protocol: 'https')
stub_config_setting(relative_url_root: '/gitlab')
node = GeoNode.new
expect(node.url).to eq('https://localhost/gitlab/')
expect(empty_node.url).to eq('https://localhost/gitlab/')
end
end
describe '#url=' do
subject { GeoNode.new }
before do
subject.url = dummy_url
end
subject { new_node }
it 'sets schema field based on url' do
expect(subject.uri.scheme).to eq('https')
......@@ -270,20 +384,100 @@ describe GeoNode, type: :model do
expect(subject.uri.port).to eq(3000)
end
context 'when unspecified ports' do
context 'when using unspecified ports' do
let(:dummy_http) { 'http://example.com/' }
let(:dummy_https) { 'https://example.com/' }
context 'when schema is http' do
it 'sets port 80' do
subject.url = dummy_http
expect(subject.uri.port).to eq(80)
end
end
context 'when schema is https' do
it 'sets port 443' do
subject.url = dummy_https
expect(subject.uri.port).to eq(443)
end
end
end
end
describe '#alternate_uri' do
let(:alternate_url) { 'https://foo:3003/bar' }
let(:node) { create(:geo_node, url: 'https://localhost:3000/gitlab', alternate_url: alternate_url) }
context 'when all fields are filled' do
it 'returns an URI object' do
expect(node.alternate_uri).to be_a URI
end
it 'includes schema, host, port and relative_url_root with a terminating /' do
expected_uri = URI.parse(alternate_url)
expected_uri.path += '/'
expect(node.alternate_uri).to eq(expected_uri)
end
end
end
describe '#alternate_url' do
let(:alternate_url) { 'https://foo:3003/bar' }
let(:node) { create(:geo_node, url: 'https://localhost:3000/gitlab', alternate_url: alternate_url) }
it 'returns a string' do
expect(node.alternate_url).to be_a String
end
it 'includes schema home port and relative_url with a terminating /' do
expected_url = "#{alternate_url}/"
expect(node.alternate_url).to eq(expected_url)
end
it 'can be nil' do
stub_config_setting(port: 443)
stub_config_setting(protocol: 'https')
stub_config_setting(relative_url_root: '/gitlab')
expect(empty_node.alternate_url).to be_nil
end
end
describe '#alternate_url=' do
subject { GeoNode.new(alternate_url: 'https://foo:3003/bar') }
it 'sets schema field based on url' do
expect(subject.alternate_uri.scheme).to eq('https')
end
it 'sets host field based on url' do
expect(subject.alternate_uri.host).to eq('foo')
end
it 'sets port field based on specified by url' do
expect(subject.alternate_uri.port).to eq(3003)
end
context 'when using unspecified ports' do
let(:dummy_http) { 'http://example.com/' }
let(:dummy_https) { 'https://example.com/' }
it 'sets port 80 when http and no port is specified' do
subject.url = dummy_http
context 'when schema is http' do
it 'sets port 80' do
subject.alternate_url = dummy_http
expect(subject.uri.port).to eq(80)
expect(subject.alternate_uri.port).to eq(80)
end
end
it 'sets port 443 when https and no port is specified' do
subject.url = dummy_https
context 'when schema is https' do
it 'sets port 443' do
subject.alternate_url = dummy_https
expect(subject.uri.port).to eq(443)
expect(subject.alternate_uri.port).to eq(443)
end
end
end
end
......@@ -342,6 +536,20 @@ describe GeoNode, type: :model do
end
end
describe '#alternate_oauth_callback_url' do
let(:node) { create(:geo_node, url: 'https://localhost:3000/gitlab', alternate_url: 'https://alternate:4444/gitlabalternate') }
let(:alternate_oauth_callback_url) { 'https://alternate:4444/gitlabalternate/oauth/geo/callback' }
it 'returns oauth callback url based on node uri' do
expect(node.alternate_oauth_callback_url).to eq(alternate_oauth_callback_url)
end
it 'returns url that matches rails url_helpers generated one' do
route = url_helpers.oauth_geo_callback_url(protocol: 'https:', host: 'alternate', port: 4444, script_name: '/gitlabalternate')
expect(node.alternate_oauth_callback_url).to eq(route)
end
end
describe '#oauth_logout_url' do
let(:fake_state) { CGI.escape('fakestate') }
let(:oauth_logout_url) { "https://localhost:3000/gitlab/oauth/geo/logout?state=#{fake_state}" }
......
......@@ -212,6 +212,7 @@ describe API::GeoNodes, :geo, :prometheus, api: true do
params = {
enabled: false,
url: 'https://updated.example.com/',
alternate_url: 'https://alternate.example.com/',
files_max_capacity: 33,
repos_max_capacity: 44,
verification_max_capacity: 55
......
......@@ -1923,9 +1923,6 @@ msgstr ""
msgid "Choose what content you want to see on a group’s overview page"
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."
msgstr ""
......@@ -2888,18 +2885,9 @@ msgstr ""
msgid "Control the display of third party offers."
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"
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"
msgstr ""
......@@ -4572,6 +4560,9 @@ msgstr ""
msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
msgstr ""
msgid "GeoNodes|Alternate URL"
msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
......@@ -4752,9 +4743,24 @@ msgstr ""
msgid "Geo|All projects are being scheduled for re-sync"
msgstr ""
msgid "Geo|Alternate URL"
msgstr ""
msgid "Geo|Batch operations"
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."
msgstr ""
......@@ -4842,6 +4848,9 @@ msgstr ""
msgid "Geo|Select groups to replicate."
msgstr ""
msgid "Geo|Selective synchronization"
msgstr ""
msgid "Geo|Shards to synchronize"
msgstr ""
......@@ -4854,12 +4863,21 @@ msgstr ""
msgid "Geo|Synchronization failed - %{error}"
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."
msgstr ""
msgid "Geo|Tracking entry will be removed. Are you sure?"
msgstr ""
msgid "Geo|URL"
msgstr ""
msgid "Geo|Unknown state"
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."
msgstr ""
msgid "Selective synchronization"
msgstr ""
msgid "Send email"
msgstr ""
......
......@@ -7,7 +7,7 @@ module QA
class New < QA::Page::Base
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_placeholder, "label :url, 'URL'" # rubocop:disable QA/ElementWithPattern
element :node_url_placeholder, "label :url" # rubocop:disable QA/ElementWithPattern
end
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