Commit eb6206b6 authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into ce-to-ee-2017-11-29

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents 5497ee6a 689b3119
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
import Clipboard from 'clipboard';
var genericError, genericSuccess, showTooltip;
function showTooltip(target, title) {
const $target = $(target);
const originalTitle = $target.data('original-title');
if (!$target.data('hideTooltip')) {
$target
.attr('title', title)
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
.tooltip('fixTitle');
}
}
genericSuccess = function(e) {
function genericSuccess(e) {
showTooltip(e.trigger, 'Copied');
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
return $(e.trigger).blur();
};
$(e.trigger).blur();
}
// Safari doesn't support `execCommand`, so instead we inform the user to
// copy manually.
//
// See http://clipboardjs.com/#browser-support
genericError = function(e) {
var key;
/**
* Safari > 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
* See http://clipboardjs.com/#browser-support
*/
function genericError(e) {
let key;
if (/Mac/i.test(navigator.userAgent)) {
key = '&#8984;'; // Command
} else {
key = 'Ctrl';
}
return showTooltip(e.trigger, "Press " + key + "-C to copy");
};
showTooltip = function(target, title) {
var $target = $(target);
var originalTitle = $target.data('original-title');
if (!$target.data('hideTooltip')) {
$target
.attr('title', 'Copied')
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
.tooltip('fixTitle');
}
};
showTooltip(e.trigger, `Press ${key}-C to copy`);
}
$(function() {
export default function initCopyToClipboard() {
const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
clipboard.on('success', genericSuccess);
clipboard.on('error', genericError);
// This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM.
// The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and `gfm` keys into the `data-clipboard-text`
// attribute that ClipboardJS reads from.
// When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly` attribute`), sets its value
// to the value of this data attribute, focusses on it, and finally programmatically issues the 'Copy' command,
// this code intercepts the copy command/event at the last minute to deconstruct this JSON hash and set the
// `text/plain` and `text/x-gfm` copy data types to the intended values.
$(document).on('copy', 'body > textarea[readonly]', function(e) {
/**
* This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting
* of plain text or GFM. The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and
* `gfm` keys into the `data-clipboard-text` attribute that ClipboardJS reads from.
* When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly`
* attribute`), sets its value to the value of this data attribute, focusses on it, and finally
* programmatically issues the 'Copy' command, this code intercepts the copy command/event at
* the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy
* data types to the intended values.
*/
$(document).on('copy', 'body > textarea[readonly]', (e) => {
const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
......@@ -71,4 +70,4 @@ $(function() {
clipboardData.setData('text/plain', json.text);
clipboardData.setData('text/x-gfm', json.gfm);
});
});
}
import './autosize';
import './bind_in_out';
import initCopyAsGFM from './copy_as_gfm';
import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
......@@ -9,3 +10,4 @@ import './toggler_behavior';
installGlEmojiElement();
initCopyAsGFM();
initCopyToClipboard();
......@@ -44,7 +44,6 @@ import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
import './copy_to_clipboard';
import './diff';
import './files_comment_button';
import Flash, { removeFlashClickListener } from './flash';
......
......@@ -17,13 +17,14 @@ export default class Project {
$('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
const activeText = $this.find('.dropdown-menu-inner-title').text();
e.preventDefault();
$('.is-active', $cloneOptions).not($this).removeClass('is-active');
$this.toggleClass('is-active');
$projectCloneField.val(url);
$cloneBtnText.text($this.text());
$cloneBtnText.text(activeText);
$('#modal-geo-info').data({
cloneUrlSecondary: $this.attr('href'),
......
......@@ -66,10 +66,18 @@ export default {
:class="{active : tab.active }"
:title="tab.url"
>
<<<<<<< HEAD
{{tab.name}}
<fileStatusIcon
:file="tab">
</fileStatusIcon>
</a>
=======
{{ tab.name }}
<file-status-icon
:file="tab"
/>
</div>
>>>>>>> origin/master
</li>
</template>
.ci-status-icon-success,
.ci-status-icon-passed {
<<<<<<< HEAD
&,
&:hover,
&:focus {
color: $green-500;
=======
svg {
fill: $green-500;
>>>>>>> origin/master
}
}
.ci-status-icon-failed {
<<<<<<< HEAD
&,
&:hover,
&:focus {
color: $gl-danger;
=======
svg {
fill: $gl-danger;
>>>>>>> origin/master
}
}
.ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
<<<<<<< HEAD
&,
&:hover,
&:focus {
color: $orange-500;
=======
svg {
fill: $orange-500;
>>>>>>> origin/master
}
}
.ci-status-icon-running {
<<<<<<< HEAD
&,
&:hover,
&:focus {
color: $blue-400;
=======
svg {
fill: $blue-400;
>>>>>>> origin/master
}
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
<<<<<<< HEAD
&,
&:hover,
&:focus {
color: $gl-text-color;
=======
svg {
fill: $gl-text-color;
>>>>>>> origin/master
}
}
.ci-status-icon-created,
.ci-status-icon-skipped {
<<<<<<< HEAD
&,
&:hover,
&:focus {
color: $gray-darkest;
=======
svg {
fill: $gray-darkest;
>>>>>>> origin/master
}
}
.ci-status-icon-manual {
<<<<<<< HEAD
&,
&:hover,
&:focus {
color: $gl-text-color;
=======
svg {
fill: $gl-text-color;
>>>>>>> origin/master
}
}
......
......@@ -403,6 +403,18 @@
}
}
}
.clone-dropdown-btn {
background-color: $white-light;
}
.clone-options-dropdown {
min-width: 240px;
.dropdown-menu-inner-content {
min-width: 320px;
}
}
}
.project-repo-buttons {
......
......@@ -46,6 +46,7 @@
max-width: 250px;
}
}
<<<<<<< HEAD
}
.ide-file-list {
......@@ -54,12 +55,127 @@
.file {
cursor: pointer;
=======
.file-status-icon {
width: 10px;
height: 10px;
>>>>>>> origin/master
}
}
<<<<<<< HEAD
a {
color: $gl-text-color;
}
th {
position: sticky;
top: 0;
=======
.ide-file-list {
flex: 1;
overflow: scroll;
.file {
cursor: pointer;
}
a {
color: $gl-text-color;
>>>>>>> origin/master
}
}
<<<<<<< HEAD
.multi-file-table-name,
.multi-file-table-col-commit-message {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 0;
}
.multi-file-table-name {
width: 350px;
}
.multi-file-table-col-commit-message {
width: 50%;
}
.multi-file-edit-pane {
display: flex;
flex-direction: column;
flex: 1;
border-left: 1px solid $white-dark;
overflow: hidden;
}
.multi-file-tabs {
display: flex;
overflow: scroll;
background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark;
> li {
position: relative;
}
}
.multi-file-tab {
@include str-truncated(150px);
padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
background-color: $gray-normal;
border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
cursor: pointer;
&.active {
background-color: $white-light;
border-bottom-color: $white-light;
}
}
.multi-file-tab-close {
position: absolute;
right: 8px;
top: 50%;
padding: 0;
background: none;
border: 0;
font-size: $gl-font-size;
color: $gray-darkest;
transform: translateY(-50%);
&:not(.modified):hover,
&:not(.modified):focus {
color: $hint-color;
}
&.modified {
color: $indigo-700;
}
}
.multi-file-edit-pane-content {
flex: 1;
height: 0;
}
.multi-file-editor-btn-group {
padding: $grid-size;
border-top: 1px solid $white-dark;
}
// Not great, but this is to deal with our current output
.multi-file-preview-holder {
height: 100%;
overflow: scroll;
.blob-viewer {
height: 100%;
=======
th {
position: sticky;
top: 0;
......@@ -133,9 +249,15 @@
&.modified {
color: $indigo-700;
>>>>>>> origin/master
}
}
<<<<<<< HEAD
.file-content.code {
display: flex;
=======
.multi-file-edit-pane-content {
flex: 1;
height: 0;
......@@ -158,6 +280,7 @@
.file-content.code {
display: flex;
>>>>>>> origin/master
i {
margin-left: -10px;
}
......@@ -262,6 +385,19 @@
.multi-file-commit-list-path {
@include str-truncated(100%);
}
<<<<<<< HEAD
.multi-file-commit-form {
padding-top: 12px;
border-top: 1px solid $white-dark;
}
.multi-file-commit-fieldset {
display: flex;
align-items: center;
padding-bottom: 12px;
=======
.multi-file-commit-form {
padding-top: 12px;
......@@ -273,6 +409,7 @@
align-items: center;
padding-bottom: 12px;
>>>>>>> origin/master
.btn {
flex: 1;
}
......
......@@ -31,9 +31,9 @@ module ApplicationSettingsHelper
def enabled_project_button(project, protocol)
case protocol
when 'ssh'
ssh_clone_button(project, 'bottom', append_link: false)
ssh_clone_button(project, append_link: false)
else
http_clone_button(project, 'bottom', append_link: false)
http_clone_button(project, append_link: false)
end
end
......
......@@ -56,44 +56,41 @@ module ButtonHelper
end
end
def http_clone_button(project, placement = 'right', append_link: true)
klass = 'http-selector'
klass << ' has-tooltip' if current_user.try(:require_extra_setup_for_git_auth?)
def http_clone_button(project, append_link: true)
protocol = gitlab_config.protocol.upcase
dropdown_description = http_dropdown_description(protocol)
append_url = project.http_url_to_repo if append_link
geo_url = geo_primary_http_url_to_repo(project) if Gitlab::Geo.secondary?
tooltip_title =
if current_user.try(:require_password_creation_for_git?)
_("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
else
_("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
end
dropdown_item_with_description(protocol, dropdown_description, href: append_url, geo_url: geo_url)
end
content_tag (append_link ? :a : :span), protocol,
class: klass,
href: (project.http_url_to_repo if append_link),
data: {
html: true,
placement: placement,
container: 'body',
title: tooltip_title,
primary_url: (geo_primary_http_url_to_repo(project) if Gitlab::Geo.secondary?)
}
def http_dropdown_description(protocol)
if current_user.try(:require_password_creation_for_git?)
_("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
else
_("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
end
end
def ssh_clone_button(project, placement = 'right', append_link: true)
klass = 'ssh-selector'
klass << ' has-tooltip' if current_user.try(:require_ssh_key?)
def ssh_clone_button(project, append_link: true)
dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") if current_user.try(:require_ssh_key?)
append_url = project.ssh_url_to_repo if append_link
geo_url = geo_primary_ssh_url_to_repo(project) if Gitlab::Geo.secondary?
content_tag (append_link ? :a : :span), 'SSH',
class: klass,
href: (project.ssh_url_to_repo if append_link),
dropdown_item_with_description('SSH', dropdown_description, href: append_url, geo_url: geo_url)
end
def dropdown_item_with_description(title, description, href: nil, geo_url: nil)
button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
content_tag (href ? :a : :span),
button_content,
class: "#{title.downcase}-selector",
href: (href if href),
data: {
html: true,
placement: placement,
container: 'body',
title: _('Add an SSH key to your profile to pull or push via SSH.'),
primary_url: (geo_primary_ssh_url_to_repo(project) if Gitlab::Geo.secondary?)
primary_url: (geo_url if geo_url)
}
end
......
......@@ -8,19 +8,17 @@ class GeoNode < ActiveRecord::Base
has_many :namespaces, through: :geo_node_namespace_links
has_one :status, class_name: 'GeoNodeStatus'
default_values schema: lambda { Gitlab.config.gitlab.protocol },
host: lambda { Gitlab.config.gitlab.host },
port: lambda { Gitlab.config.gitlab.port },
relative_url_root: lambda { Gitlab.config.gitlab.relative_url_root },
default_values url: ->(record) { record.class.current_node_url },
primary: false,
clone_protocol: 'http'
accepts_nested_attributes_for :geo_node_key
validates :host, host: true, presence: true, uniqueness: { case_sensitive: false, scope: :port }
validates :url, presence: true, uniqueness: { case_sensitive: false }
validate :check_url_is_valid
validates :primary, uniqueness: { message: 'node already exists' }, if: :primary
validates :schema, inclusion: %w(http https)
validates :relative_url_root, length: { minimum: 0, allow_nil: false }
validates :access_key, presence: true
validates :encrypted_secret_access_key, presence: true
validates :clone_protocol, presence: true, inclusion: %w(ssh http)
......@@ -35,16 +33,33 @@ class GeoNode < ActiveRecord::Base
before_validation :ensure_access_keys!
scope :with_url_prefix, ->(prefix) { where('url LIKE ?', "#{prefix}%") }
attr_encrypted :secret_access_key,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-gcm',
mode: :per_attribute_iv,
encode: true
class << self
def current_node_url
RequestStore.fetch('geo_node:current_node_url') do
cfg = Gitlab.config.gitlab
uri = URI.parse("#{cfg.protocol}://#{cfg.host}:#{cfg.port}#{cfg.relative_url_root}")
uri.path += '/' unless uri.path.end_with?('/')
uri.to_s
end
end
def current_node
GeoNode.find_by(url: current_node_url)
end
end
def current?
host == Gitlab.config.gitlab.host &&
port == Gitlab.config.gitlab.port &&
relative_url_root == Gitlab.config.gitlab.relative_url_root
self.class.current_node_url == url
end
def secondary?
......@@ -55,24 +70,23 @@ class GeoNode < ActiveRecord::Base
secondary? && clone_protocol == 'ssh'
end
def uri
if relative_url_root
relative_url = relative_url_root.starts_with?('/') ? relative_url_root : "/#{relative_url_root}"
end
def url
value = read_attribute(:url)
value += '/' if value.present? && !value.end_with?('/')
URI.parse(URI::Generic.build(scheme: schema, host: host, port: port, path: relative_url).normalize.to_s)
value
end
def url
uri.to_s
def url=(value)
value += '/' if value.present? && !value.end_with?('/')
write_attribute(:url, value)
@uri = nil
end
def url=(new_url)
new_uri = URI.parse(new_url)
self.schema = new_uri.scheme
self.host = new_uri.host
self.port = new_uri.port
self.relative_url_root = new_uri.path != '/' ? new_uri.path : ''
def uri
@uri ||= URI.parse(url) if url.present?
end
def geo_transfers_url(file_type, file_id)
......@@ -203,7 +217,7 @@ class GeoNode < ActiveRecord::Base
private
def geo_api_url(suffix)
URI.join(uri, "#{uri.path}/", "api/#{API::API.version}/geo/#{suffix}").to_s
URI.join(uri, "#{uri.path}", "api/#{API::API.version}/geo/#{suffix}").to_s
end
def ensure_access_keys!
......@@ -216,11 +230,7 @@ class GeoNode < ActiveRecord::Base
end
def url_helper_args
if relative_url_root
relative_url = relative_url_root.starts_with?('/') ? relative_url_root : "/#{relative_url_root}"
end
{ protocol: schema, host: host, port: port, script_name: relative_url }
{ protocol: uri.scheme, host: uri.host, port: uri.port, script_name: uri.path }
end
def build_dependents
......@@ -246,13 +256,19 @@ class GeoNode < ActiveRecord::Base
# Prevent locking yourself out
def check_not_adding_primary_as_secondary
if host == Gitlab.config.gitlab.host &&
port == Gitlab.config.gitlab.port &&
relative_url_root == Gitlab.config.gitlab.relative_url_root
if url == self.class.current_node_url
errors.add(:base, 'Current node must be the primary node or you will be locking yourself out')
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')
end
rescue URI::InvalidURIError
errors.add(:url, 'is invalid')
end
def update_clone_url
self.clone_url_prefix = Gitlab.config.gitlab_shell.ssh_path_prefix
end
......
......@@ -7,7 +7,7 @@
%span
= enabled_project_button(project, enabled_protocol)
- else
%a#clone-dropdown.clone-dropdown-btn.btn{ href: '#', data: { toggle: 'dropdown' } }
%a#clone-dropdown.btn.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown' } }
%span
= default_clone_protocol.upcase
= icon('caret-down')
......
---
title: Fix Advanced Search Syntax documentation
merge_request: 3571
author:
type: fixed
---
title: Handle outdated replicas in the DB load balancer
merge_request:
author:
type: added
---
title: Removed tooltip from clone dropdown
merge_request: 15334
author:
type: other
class StoreGeoNodesUrlDirectly < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
class GeoNode < ActiveRecord::Base
def compute_unified_url!
uri = URI.parse("#{schema}://#{host}#{relative_url_root}")
uri.port = port if port.present?
uri.path += '/' unless uri.path.end_with?('/')
update!(url: uri.to_s)
end
def compute_split_url!
uri = URI.parse(url)
update!(
schema: uri.scheme,
host: uri.host,
port: uri.port,
relative_url_root: uri.path
)
end
end
def up
add_column :geo_nodes, :url, :string
GeoNode.find_each { |node| node.compute_unified_url! }
change_column_null(:geo_nodes, :url, false)
end
def down
GeoNode.find_each { |node| node.compute_split_url! }
remove_column :geo_nodes, :url, :string
end
end
class IndexGeoNodesUrl < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :geo_nodes, :url, unique: true
end
def down
remove_concurrent_index :geo_nodes, :url, unique: true
end
end
class RemoveGeoNodesUrlPartColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
remove_column :geo_nodes, :schema, :string
remove_column :geo_nodes, :host, :string
remove_column :geo_nodes, :port, :integer
remove_column :geo_nodes, :relative_url_root, :string
end
def down
add_column :geo_nodes, :schema, :string
add_column :geo_nodes, :host, :string
add_column :geo_nodes, :port, :integer
add_column :geo_nodes, :relative_url_root, :string
add_concurrent_index :geo_nodes, :host
end
end
......@@ -11,7 +11,11 @@
#
# It's strongly recommended that you check this file into your version control system.
<<<<<<< HEAD
ActiveRecord::Schema.define(version: 20171124150326) do
=======
ActiveRecord::Schema.define(version: 20171124165823) do
>>>>>>> origin/master
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -959,10 +963,6 @@ ActiveRecord::Schema.define(version: 20171124150326) do
add_index "geo_node_statuses", ["geo_node_id"], name: "index_geo_node_statuses_on_geo_node_id", unique: true, using: :btree
create_table "geo_nodes", force: :cascade do |t|
t.string "schema"
t.string "host"
t.integer "port"
t.string "relative_url_root"
t.boolean "primary"
t.integer "geo_node_key_id"
t.integer "oauth_application_id"
......@@ -974,11 +974,12 @@ ActiveRecord::Schema.define(version: 20171124150326) do
t.integer "files_max_capacity", default: 10, null: false
t.integer "repos_max_capacity", default: 25, null: false
t.string "clone_protocol", default: "http", null: false
t.string "url", null: false
end
add_index "geo_nodes", ["access_key"], name: "index_geo_nodes_on_access_key", using: :btree
add_index "geo_nodes", ["host"], name: "index_geo_nodes_on_host", using: :btree
add_index "geo_nodes", ["primary"], name: "index_geo_nodes_on_primary", using: :btree
add_index "geo_nodes", ["url"], name: "index_geo_nodes_on_url", unique: true, using: :btree
create_table "geo_repositories_changed_events", id: :bigserial, force: :cascade do |t|
t.integer "geo_node_id", null: false
......
......@@ -156,6 +156,40 @@ log entries easier. For example:
[DB-LB] Host 10.123.2.7 came back online
```
## Handling Stale Reads
> [Introduced][ee-3526] in [GitLab Enterprise Edition Premium][eep] 10.3.
To prevent reading from an outdated secondary the load balancer will check if it
is in sync with the primary. If the data is determined to be recent enough the
secondary can be used, otherwise it will be ignored. To reduce the overhead of
these checks we only perform these checks at certain intervals.
There are three configuration options that influence this behaviour:
| Option | Description | Default |
|------------------------------|----------------------------------------------------------------------------------------------------------------|------------|
| `max_replication_difference` | The amount of data (in bytes) a secondary is allowed to lag behind when it hasn't replicated data for a while. | 8 MB |
| `max_replication_lag_time` | The maximum number of seconds a secondary is allowed to lag behind before we stop using it. | 60 seconds |
| `replica_check_interval` | The minimum number of seconds we have to wait before checking the status of a secondary. | 60 seconds |
The defaults should be sufficient for most users. Should you want to change them
you can specify them in `config/database.yml` like so:
```yaml
production:
username: gitlab
database: gitlab
encoding: unicode
load_balancing:
hosts:
- host1.example.com
- host2.example.com
max_replication_difference: 16777216 # 16 MB
max_replication_lag_time: 30
replica_check_interval: 30
```
[hot-standby]: https://www.postgresql.org/docs/9.6/static/hot-standby.html
[ee-1283]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1283
[eep]: https://about.gitlab.com/gitlab-ee/
......@@ -163,3 +197,4 @@ log entries easier. For example:
[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
[wikipedia]: https://en.wikipedia.org/wiki/Load_balancing_(computing)
[db-req]: ../install/requirements.md#database
[ee-3526]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3526
......@@ -7,7 +7,7 @@ work for on-premises installations where you can configure the
with Slack on making this configurable for all GitLab installations, but there's
no ETA.
It was first introduced in GitLab 9.4 and distributed to Slack App Directory in
GitLab 10.2 (with availability toward the end of November 2017).
GitLab 10.2.
Slack provides a native application which you can enable via your project's
integrations on GitLab.com.
......@@ -15,12 +15,9 @@ integrations on GitLab.com.
## Slack App Directory
The simplest way to enable the GitLab Slack application for your workspace is to
install the GitLab application from
install the [GitLab application](https://slack-platform.slack.com/apps/A676ADMV5-gitlab) from
the [Slack App Directory](https://slack.com/apps).
> This will be available toward the end of November 2017, and the docs will be updated here
when it is ready.
Clicking install will take you to the
[GitLab Slack application landing page](https://gitlab.com/profile/slack/edit)
where you can select a project to enable the GitLab Slack application for.
......
......@@ -23,13 +23,13 @@ you need the search results to be as efficient as possible. You have a feeling
of what you want to find (e.g., a function name), but at the same you're also
not so sure.
In that case, using the regular expressions in your query will yield much better
results.
In that case, using the advanced search syntax in your query will yield much
better results.
## Using the Advanced Syntax Search
The Advanced Syntax Search supports queries of ranges, wildcards, regular
expressions, fuzziness and much more.
The Advanced Syntax Search supports fuzzy or exact search queries with prefixes,
boolean operators, and much more.
Full details can be found in the [Elasticsearch documentation][elastic], but
here's a quick guide:
......@@ -42,7 +42,6 @@ here's a quick guide:
* To group terms together, use parentheses: `bug | (display +sound)`
* To match a partial word, use `*`: `bug find_by_*`
* To find a term containing one of these symbols, use `\`: `argument \-last`
* To limit the results based on the time "created_at:[2012-01-01 TO 2012-12-31]" and other sweet stuff
[ee]: https://about.gitlab.com/gitlab-ee/
[elastic]: https://www.elastic.co/guide/en/elasticsearch/reference/5.3/query-dsl-simple-query-string-query.html#_simple_query_string_syntax
......@@ -110,7 +110,7 @@ class Admin::GeoNodesController < Admin::ApplicationController
end
def has_insecure_nodes?
GeoNode.where(schema: 'http').any?
GeoNode.with_url_prefix('http://').exists?
end
def flash_now(type, message)
......
......@@ -39,8 +39,14 @@ module EE
def redirect_allowed_to?(uri)
raise NotImplementedError unless defined?(super)
# Redirect is not only allowed to current host, but also to other Geo nodes
super || ::Gitlab::Geo.geo_node?(host: uri.host, port: uri.port)
# Redirect is not only allowed to current host, but also to other Geo
# nodes. relative_url_root *must* be ignored here as we don't know what
# is root and what is path
super || begin
truncated = uri.dup.tap { |uri| uri.path = '/' }
::GeoNode.with_url_prefix(truncated).exists?
end
end
end
end
......@@ -24,15 +24,30 @@ module Gitlab
[].freeze
end
# Returns a Hash containing the load balancing configuration.
def self.configuration
ActiveRecord::Base.configurations[Rails.env]['load_balancing'] || {}
end
# Returns the maximum replica lag size in bytes.
def self.max_replication_difference
(configuration['max_replication_difference'] || 8.megabytes).to_i
end
# Returns the maximum lag time for a replica.
def self.max_replication_lag_time
(configuration['max_replication_lag_time'] || 60.0).to_f
end
# Returns the interval (in seconds) to use for checking the status of a
# replica.
def self.replica_check_interval
(configuration['replica_check_interval'] || 60).to_f
end
# Returns the additional hosts to use for load balancing.
def self.hosts
hash = ActiveRecord::Base.configurations[Rails.env]['load_balancing']
if hash
hash['hosts'] || []
else
[]
end
configuration['hosts'] || []
end
def self.log(level, message)
......
......@@ -3,15 +3,21 @@ module Gitlab
module LoadBalancing
# A single database host used for load balancing.
class Host
attr_reader :pool
attr_reader :pool, :last_checked_at, :intervals, :load_balancer
delegate :connection, :release_connection, to: :pool
# host - The address of the database.
def initialize(host)
# load_balancer - The LoadBalancer that manages this Host.
def initialize(host, load_balancer)
@host = host
@load_balancer = load_balancer
@pool = Database.create_connection_pool(LoadBalancing.pool_size, host)
@online = true
@last_checked_at = Time.zone.now
interval = LoadBalancing.replica_check_interval
@intervals = (interval..(interval * 2)).step(0.5).to_a
end
def offline!
......@@ -23,30 +29,80 @@ module Gitlab
# Returns true if the host is online.
def online?
return true if @online
begin
retried = 0
@online = begin
connection.active?
rescue
if retried < 3
release_connection
retried += 1
retry
else
false
end
end
LoadBalancing.log(:info, "Host #{@host} came back online") if @online
@online
ensure
release_connection
return @online unless check_replica_status?
refresh_status
LoadBalancing.log(:info, "Host #{@host} came back online") if @online
@online
end
def refresh_status
@online = replica_is_up_to_date?
@last_checked_at = Time.zone.now
end
def check_replica_status?
(Time.zone.now - last_checked_at) >= intervals.sample
end
def replica_is_up_to_date?
replication_lag_below_threshold? || data_is_recent_enough?
end
def replication_lag_below_threshold?
if (lag_time = replication_lag_time)
lag_time <= LoadBalancing.max_replication_lag_time
else
false
end
end
# Returns true if the replica has replicated enough data to be useful.
def data_is_recent_enough?
# It's possible for a replica to not replay WAL data for a while,
# despite being up to date. This can happen when a primary does not
# receive any writes a for a while.
#
# To prevent this from happening we check if the lag size (in bytes)
# of the replica is small enough for the replica to be useful. We
# only do this if we haven't replicated in a while so we only need
# to connect to the primary when truly necessary.
if (lag_size = replication_lag_size)
lag_size <= LoadBalancing.max_replication_difference
else
false
end
end
# Returns the replication lag time of this secondary in seconds as a
# float.
#
# This method will return nil if no lag time could be calculated.
def replication_lag_time
row = query_and_release('SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))::float as lag')
row['lag'].to_f if row.any?
end
# Returns the number of bytes this secondary is lagging behind the
# primary.
#
# This method will return nil if no lag size could be calculated.
def replication_lag_size
location = connection.quote(primary_write_location)
row = query_and_release("SELECT pg_xlog_location_diff(#{location}, pg_last_xlog_replay_location())::float AS diff")
row['diff'].to_i if row.any?
end
def primary_write_location
load_balancer.primary_write_location
ensure
load_balancer.release_primary_connection
end
# Returns true if this host has caught up to the given transaction
# write location.
#
......@@ -60,9 +116,15 @@ module Gitlab
query = "SELECT NOT pg_is_in_recovery() OR " \
"pg_xlog_location_diff(pg_last_xlog_replay_location(), #{string}) >= 0 AS result"
row = connection.select_all(query).first
row = query_and_release(query)
row['result'] == 't'
end
row && row['result'] == 't'
def query_and_release(sql)
connection.select_all(sql).first || {}
rescue
{}
ensure
release_connection
end
......
......@@ -15,7 +15,7 @@ module Gitlab
# hosts - The hostnames/addresses of the additional databases.
def initialize(hosts = [])
@host_list = HostList.new(hosts.map { |addr| Host.new(addr) })
@host_list = HostList.new(hosts.map { |addr| Host.new(addr, self) })
end
# Yields a connection that can be used for reads.
......
......@@ -18,11 +18,7 @@ module Gitlab
FDW_SCHEMA = 'gitlab_secondary'.freeze
def self.current_node
self.cache_value(:geo_node_current) do
GeoNode.find_by(host: Gitlab.config.gitlab.host,
port: Gitlab.config.gitlab.port,
relative_url_root: Gitlab.config.gitlab.relative_url_root)
end
self.cache_value(:geo_node_current) { GeoNode.current_node }
end
def self.primary_node
......@@ -72,10 +68,6 @@ module Gitlab
::License.feature_available?(:geo)
end
def self.geo_node?(host:, port:)
GeoNode.where(host: host, port: port).exists?
end
def self.fdw?
self.cache_value(:geo_fdw?) do
::Geo::BaseRegistry.connection.execute(
......
......@@ -175,15 +175,7 @@ namespace :geo do
end
def set_primary_geo_node
params = {
schema: Gitlab.config.gitlab.protocol,
host: Gitlab.config.gitlab.host,
port: Gitlab.config.gitlab.port,
relative_url_root: Gitlab.config.gitlab.relative_url_root,
primary: true
}
node = GeoNode.new(params)
node = GeoNode.new(primary: true, url: GeoNode.current_node_url)
puts "Saving primary GeoNode with URL #{node.url}".color(:green)
node.save
......
......@@ -249,7 +249,7 @@ describe Admin::GeoNodesController, :postgresql do
end
context 'with a secondary node' do
let(:geo_node) { create(:geo_node, host: 'example.com', port: 80, enabled: true) }
let(:geo_node) { create(:geo_node, url: 'http://example.com') }
context 'when succeed' do
before do
......
......@@ -2,16 +2,19 @@ require 'rails_helper'
feature 'Geo clone instructions', :js do
include Devise::Test::IntegrationHelpers
include ::EE::GeoHelpers
let(:project) { create(:project, :empty_repo) }
let(:developer) { create(:user) }
background do
primary = create(:geo_node, :primary, schema: 'https', host: 'primary.domain.com', port: 443)
primary.update_attribute(:clone_url_prefix, 'git@primary.domain.com:')
allow(Gitlab::Geo).to receive(:secondary?).and_return(true)
primary = create(:geo_node, :primary, url: 'https://primary.domain.com')
primary.update_columns(clone_url_prefix: 'git@primary.domain.com:')
secondary = create(:geo_node)
project.team << [developer, :developer]
stub_current_geo_node(secondary)
project.add_developer(developer)
sign_in(developer)
end
......
......@@ -32,7 +32,6 @@ describe EE::GitlabRoutingHelper do
context 'HTTP' do
before do
allow(helper).to receive(:default_clone_protocol).and_return('http')
primary.update!(schema: 'http')
end
context 'project' do
......@@ -51,7 +50,7 @@ describe EE::GitlabRoutingHelper do
context 'HTTPS' do
before do
allow(helper).to receive(:default_clone_protocol).and_return('https')
primary.update!(schema: 'https')
primary.update!(url: 'https://localhost:123/relative')
end
context 'project' do
......
......@@ -7,9 +7,17 @@ describe RemoveSystemHookFromGeoNodes, :migration do
before do
allow_any_instance_of(WebHookService).to receive(:execute)
node_attrs = {
schema: 'http',
host: 'localhost',
port: 3000
}
create(:system_hook)
geo_nodes.create! attributes_for(:geo_node, :primary)
geo_nodes.create! attributes_for(:geo_node, system_hook_id: create(:system_hook).id)
hook_id = create(:system_hook).id
geo_nodes.create!(node_attrs.merge(primary: true))
geo_nodes.create!(node_attrs.merge(system_hook_id: hook_id, port: 3001))
end
it 'destroy all system hooks for secondary nodes' do
......
FactoryGirl.define do
factory :geo_node do
host { Gitlab.config.gitlab.host }
sequence(:port) {|n| n}
sequence(:url) do |port|
uri = URI.parse("http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.relative_url_root}")
uri.port = port
uri.path += '/' unless uri.path.end_with?('/')
uri.to_s
end
trait :ssh do
clone_protocol 'ssh'
......@@ -10,7 +15,13 @@ FactoryGirl.define do
trait :primary do
primary true
port { Gitlab.config.gitlab.port }
url do
uri = URI.parse("http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.relative_url_root}")
uri.port = Gitlab.config.gitlab.port
uri.path += '/' unless uri.path.end_with?('/')
uri.to_s
end
end
end
end
......@@ -26,9 +26,10 @@ describe ButtonHelper do
context 'when user has password automatically set' do
let(:user) { create(:user, password_automatically_set: true) }
it 'shows a password tooltip' do
expect(element.attr('class')).to include(has_tooltip_class)
expect(element.attr('data-title')).to eq('Set a password on your account to pull or push via HTTP.')
it 'shows the password text on the dropdown' do
description = element.search('.dropdown-menu-inner-content').first
expect(description.inner_text).to eq 'Set a password on your account to pull or push via HTTP.'
end
end
end
......@@ -39,17 +40,10 @@ describe ButtonHelper do
end
context 'when user has no personal access tokens' do
it 'has a personal access token tooltip ' do
expect(element.attr('class')).to include(has_tooltip_class)
expect(element.attr('data-title')).to eq('Create a personal access token on your account to pull or push via HTTP.')
end
end
context 'when user has a personal access token' do
it 'shows no tooltip' do
create(:personal_access_token, user: user)
it 'has a personal access token text on the dropdown description ' do
description = element.search('.dropdown-menu-inner-content').first
expect(element.attr('class')).not_to include(has_tooltip_class)
expect(description.inner_text).to eq 'Create a personal access token on your account to pull or push via HTTP.'
end
end
end
......@@ -63,6 +57,41 @@ describe ButtonHelper do
end
end
describe 'ssh_button' do
let(:user) { create(:user) }
let(:project) { build_stubbed(:project) }
def element
element = helper.ssh_clone_button(project)
Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
end
before do
allow(helper).to receive(:current_user).and_return(user)
end
context 'without an ssh key on the user' do
it 'shows a warning on the dropdown description' do
description = element.search('.dropdown-menu-inner-content').first
expect(description.inner_text).to eq "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
end
end
context 'with an ssh key on the user' do
before do
create(:key, user: user)
end
it 'there is no warning on the dropdown description' do
description = element.search('.dropdown-menu-inner-content').first
expect(description).to eq nil
end
end
end
describe 'clipboard_button' do
let(:user) { create(:user) }
let(:project) { build_stubbed(:project) }
......
......@@ -91,7 +91,7 @@ describe('RepoTab', () => {
});
it('renders a tooltip', () => {
expect(vm.$el.querySelector('.repo-tab span').dataset.originalTitle).toContain('Locked by testuser');
expect(vm.$el.querySelector('span').dataset.originalTitle).toContain('Locked by testuser');
});
});
......
......@@ -2,13 +2,16 @@ require 'spec_helper'
describe Gitlab::Database::LoadBalancing::HostList do
before do
allow(Gitlab::Database).to receive(:create_connection_pool)
allow(Gitlab::Database)
.to receive(:create_connection_pool)
.and_return(ActiveRecord::Base.connection_pool)
end
let(:load_balancer) { double(:load_balancer) }
let(:host_list) do
hosts = Array.new(2) do
Gitlab::Database::LoadBalancing::Host.new('localhost')
Gitlab::Database::LoadBalancing::Host.new('localhost', load_balancer)
end
described_class.new(hosts)
......
require 'spec_helper'
describe Gitlab::Database::LoadBalancing::Host do
let(:host) { described_class.new('localhost') }
describe Gitlab::Database::LoadBalancing::Host, :postgresql do
let(:load_balancer) do
Gitlab::Database::LoadBalancing::LoadBalancer.new(%w[localhost])
end
let(:host) { load_balancer.host_list.hosts.first }
before do
allow(Gitlab::Database).to receive(:create_connection_pool)
......@@ -33,68 +37,186 @@ describe Gitlab::Database::LoadBalancing::Host do
end
describe '#online?' do
let(:error) { Class.new(RuntimeError) }
context 'when the replica status is recent enough' do
it 'returns the latest status' do
Timecop.freeze do
host = described_class.new('localhost', load_balancer)
before do
allow(host.pool).to receive(:disconnect!)
expect(host).not_to receive(:refresh_status)
expect(host).to be_online
end
end
end
it 'returns true when the host is online' do
expect(host).not_to receive(:connection)
expect(host).not_to receive(:release_connection)
context 'when the replica status is outdated' do
it 'refreshes the status' do
host.offline!
expect(host.online?).to eq(true)
expect(host)
.to receive(:check_replica_status?)
.and_return(true)
expect(host).to be_online
end
end
end
it 'returns true when the host was marked as offline but is online again' do
connection = double(:connection, active?: true)
describe '#refresh_status' do
it 'refreshes the status' do
host.offline!
allow(host).to receive(:connection).and_return(connection)
expect(host)
.to receive(:replica_is_up_to_date?)
.and_call_original
host.offline!
host.refresh_status
expect(host).to receive(:release_connection)
expect(host.online?).to eq(true)
expect(host).to be_online
end
end
it 'returns false when the host is offline' do
connection = double(:connection, active?: false)
describe '#check_replica_status?' do
it 'returns true when we need to check the replica status' do
allow(host)
.to receive(:last_checked_at)
.and_return(1.year.ago)
allow(host).to receive(:connection).and_return(connection)
expect(host).to receive(:release_connection)
expect(host.check_replica_status?).to eq(true)
end
host.offline!
it 'returns false when we do not need to check the replica status' do
Timecop.freeze do
allow(host)
.to receive(:last_checked_at)
.and_return(Time.zone.now)
expect(host.online?).to eq(false)
expect(host.check_replica_status?).to eq(false)
end
end
end
it 'returns false when a connection could not be established' do
expect(host).to receive(:connection).exactly(4).times.and_raise(error)
expect(host).to receive(:release_connection).exactly(4).times
describe '#replica_is_up_to_date?' do
context 'when the lag time is below the threshold' do
it 'returns true' do
expect(host)
.to receive(:replication_lag_below_threshold?)
.and_return(true)
host.offline!
expect(host.online?).to eq(false)
expect(host.replica_is_up_to_date?).to eq(true)
end
end
it 'retries when a connection error is thrown' do
connection = double(:connection, active?: true)
raised = false
context 'when the lag time exceeds the threshold' do
before do
allow(host)
.to receive(:replication_lag_below_threshold?)
.and_return(false)
end
allow(host).to receive(:connection) do
unless raised
raised = true
raise error.new
end
it 'returns true if the data is recent enough' do
expect(host)
.to receive(:data_is_recent_enough?)
.and_return(true)
connection
expect(host.replica_is_up_to_date?).to eq(true)
end
expect(host).to receive(:release_connection).twice
it 'returns false when the data is not recent enough' do
expect(host)
.to receive(:data_is_recent_enough?)
.and_return(false)
host.offline!
expect(host.replica_is_up_to_date?).to eq(false)
end
end
end
describe '#replication_lag_below_threshold' do
it 'returns true when the lag time is below the threshold' do
expect(host)
.to receive(:replication_lag_time)
.and_return(1)
expect(host.online?).to eq(true)
expect(host.replication_lag_below_threshold?).to eq(true)
end
it 'returns false when the lag time exceeds the threshold' do
expect(host)
.to receive(:replication_lag_time)
.and_return(9000)
expect(host.replication_lag_below_threshold?).to eq(false)
end
it 'returns false when no lag time could be calculated' do
expect(host)
.to receive(:replication_lag_time)
.and_return(nil)
expect(host.replication_lag_below_threshold?).to eq(false)
end
end
describe '#data_is_recent_enough?' do
it 'returns true when the data is recent enough' do
expect(host.data_is_recent_enough?).to eq(true)
end
it 'returns false when the data is not recent enough' do
diff = Gitlab::Database::LoadBalancing.max_replication_difference * 2
expect(host)
.to receive(:query_and_release)
.and_return({ 'diff' => diff })
expect(host.data_is_recent_enough?).to eq(false)
end
it 'returns false when no lag size could be calculated' do
expect(host)
.to receive(:replication_lag_size)
.and_return(nil)
expect(host.data_is_recent_enough?).to eq(false)
end
end
describe '#replication_lag_time' do
it 'returns the lag time as a Float' do
expect(host.replication_lag_time).to be_an_instance_of(Float)
end
it 'returns nil when the database query returned no rows' do
expect(host)
.to receive(:query_and_release)
.and_return({})
expect(host.replication_lag_time).to be_nil
end
end
describe '#replication_lag_size' do
it 'returns the lag size as an Integer' do
# On newer versions of Ruby the class is Integer, but on CI we run a
# version that still uses Fixnum.
classes = [Fixnum, Integer] # rubocop: disable Lint/UnifiedInteger
expect(classes).to include(host.replication_lag_size.class)
end
it 'returns nil when the database query returned no rows' do
expect(host)
.to receive(:query_and_release)
.and_return({})
expect(host.replication_lag_size).to be_nil
end
end
describe '#primary_write_location' do
it 'returns the write location of the primary' do
expect(host.primary_write_location).to be_an_instance_of(String)
expect(host.primary_write_location).not_to be_empty
end
end
......@@ -119,4 +241,29 @@ describe Gitlab::Database::LoadBalancing::Host do
expect(host.caught_up?('foo')).to eq(false)
end
end
describe '#query_and_release' do
it 'executes a SQL query' do
results = host.query_and_release('SELECT 10 AS number')
expect(results).to be_an_instance_of(Hash)
expect(results['number'].to_i).to eq(10)
end
it 'releases the connection after running the query' do
expect(host)
.to receive(:release_connection)
.once
host.query_and_release('SELECT 10 AS number')
end
it 'returns an empty Hash in the event of an error' do
expect(host.connection)
.to receive(:select_all)
.and_raise(RuntimeError, 'kittens')
expect(host.query_and_release('SELECT 10 AS number')).to eq({})
end
end
end
......@@ -9,10 +9,89 @@ describe Gitlab::Database::LoadBalancing do
end
end
describe '.configuration' do
it 'returns a Hash' do
config = { 'hosts' => %w(foo) }
allow(ActiveRecord::Base.configurations[Rails.env])
.to receive(:[])
.with('load_balancing')
.and_return(config)
expect(described_class.configuration).to eq(config)
end
end
describe '.max_replication_difference' do
context 'without an explicitly configured value' do
it 'returns the default value' do
allow(described_class)
.to receive(:configuration)
.and_return({})
expect(described_class.max_replication_difference).to eq(8.megabytes)
end
end
context 'with an explicitly configured value' do
it 'returns the configured value' do
allow(described_class)
.to receive(:configuration)
.and_return({ 'max_replication_difference' => 4 })
expect(described_class.max_replication_difference).to eq(4)
end
end
end
describe '.max_replication_lag_time' do
context 'without an explicitly configured value' do
it 'returns the default value' do
allow(described_class)
.to receive(:configuration)
.and_return({})
expect(described_class.max_replication_lag_time).to eq(60)
end
end
context 'with an explicitly configured value' do
it 'returns the configured value' do
allow(described_class)
.to receive(:configuration)
.and_return({ 'max_replication_lag_time' => 4 })
expect(described_class.max_replication_lag_time).to eq(4)
end
end
end
describe '.replica_check_interval' do
context 'without an explicitly configured value' do
it 'returns the default value' do
allow(described_class)
.to receive(:configuration)
.and_return({})
expect(described_class.replica_check_interval).to eq(60)
end
end
context 'with an explicitly configured value' do
it 'returns the configured value' do
allow(described_class)
.to receive(:configuration)
.and_return({ 'replica_check_interval' => 4 })
expect(described_class.replica_check_interval).to eq(4)
end
end
end
describe '.hosts' do
it 'returns a list of hosts' do
allow(ActiveRecord::Base.configurations[Rails.env]).to receive(:[])
.with('load_balancing')
allow(described_class)
.to receive(:configuration)
.and_return({ 'hosts' => %w(foo bar baz) })
expect(described_class.hosts).to eq(%w(foo bar baz))
......
......@@ -149,16 +149,6 @@ describe Gitlab::Geo, :geo do
end
end
describe 'geo_node?' do
it 'returns true if a node with specific host and port exists' do
expect(described_class.geo_node?(host: primary_node.host, port: primary_node.port)).to be_truthy
end
it 'returns false if specified host and port doesnt match any existing node' do
expect(described_class.geo_node?(host: 'inexistent', port: 1234)).to be_falsey
end
end
describe 'license_allows?' do
it 'returns true if license has Geo addon' do
stub_licensed_features(geo: true)
......
......@@ -4,8 +4,8 @@ describe GeoNode, type: :model do
using RSpec::Parameterized::TableSyntax
include ::EE::GeoHelpers
let(:new_node) { create(:geo_node, schema: 'https', host: 'localhost', port: 3000, relative_url_root: 'gitlab') }
let(:new_primary_node) { create(:geo_node, :primary, schema: 'https', host: 'localhost', port: 3000, relative_url_root: 'gitlab') }
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(:empty_node) { described_class.new }
let(:primary_node) { create(:geo_node, :primary) }
let(:node) { create(:geo_node) }
......@@ -34,10 +34,7 @@ describe GeoNode, type: :model do
let(:gitlab_host) { 'gitlabhost' }
where(:attribute, :value) do
:schema | 'http'
:host | 'gitlabhost'
:port | 80
:relative_url_root | ''
:url | Gitlab::Routing.url_helpers.root_url
:primary | false
:repos_max_capacity | 25
:files_max_capacity | 10
......@@ -45,22 +42,13 @@ describe GeoNode, type: :model do
end
with_them do
before do
allow(Gitlab.config.gitlab).to receive(:host) { gitlab_host }
end
it { expect(empty_node[attribute]).to eq(value) }
end
end
context 'prevent locking yourself out' do
it 'does not accept adding a non primary node with same details as current_node' do
node = GeoNode.new(
host: Gitlab.config.gitlab.host,
port: Gitlab.config.gitlab.port,
relative_url_root: Gitlab.config.gitlab.relative_url_root,
geo_node_key: build(:geo_node_key)
)
node = build(:geo_node, :primary, primary: false)
expect(node).not_to be_valid
expect(node.errors.full_messages.count).to eq(1)
......@@ -129,18 +117,16 @@ describe GeoNode, type: :model do
end
describe '#current?' do
subject { described_class.new }
it 'returns true when node is the current node' do
stub_current_geo_node(subject)
node = described_class.new(url: described_class.current_node_url)
expect(subject.current?).to eq true
expect(node.current?).to be_truthy
end
it 'returns false when node is not the current node' do
subject.port = Gitlab.config.gitlab.port + 1
node = described_class.new(url: 'http://another.node.com:8080/foo')
expect(subject.current?).to eq false
expect(node.current?).to be_falsy
end
end
......@@ -150,8 +136,9 @@ describe GeoNode, type: :model do
expect(new_node.uri).to be_a URI
end
it 'includes schema home port and relative_url' do
it 'includes schema, host, port and relative_url_root with a terminating /' do
expected_uri = URI.parse(dummy_url)
expected_uri.path += '/'
expect(new_node.uri).to eq(expected_uri)
end
end
......@@ -172,18 +159,18 @@ describe GeoNode, type: :model do
expect(new_node.url).to be_a String
end
it 'includes schema home port and relative_url' do
expected_url = 'https://localhost:3000/gitlab'
it 'includes schema home port and relative_url with a terminating /' do
expected_url = 'https://localhost:3000/gitlab/'
expect(new_node.url).to eq(expected_url)
end
it 'defaults to existing HTTPS and relative URL if present' do
it 'defaults to existing HTTPS and relative URL with a terminating / if present' 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(node.url).to eq('https://localhost/gitlab/')
end
end
......@@ -195,15 +182,15 @@ describe GeoNode, type: :model do
end
it 'sets schema field based on url' do
expect(subject.schema).to eq('https')
expect(subject.uri.scheme).to eq('https')
end
it 'sets host field based on url' do
expect(subject.host).to eq('localhost')
expect(subject.uri.host).to eq('localhost')
end
it 'sets port field based on specified by url' do
expect(subject.port).to eq(3000)
expect(subject.uri.port).to eq(3000)
end
context 'when unspecified ports' do
......@@ -212,12 +199,14 @@ describe GeoNode, type: :model do
it 'sets port 80 when http and no port is specified' do
subject.url = dummy_http
expect(subject.port).to eq(80)
expect(subject.uri.port).to eq(80)
end
it 'sets port 443 when https and no port is specified' do
subject.url = dummy_https
expect(subject.port).to eq(443)
expect(subject.uri.port).to eq(443)
end
end
end
......
......@@ -17,7 +17,7 @@ describe Geo::BaseSyncService do
end
describe '#primary_ssh_path_prefix' do
let!(:primary_node) { create(:geo_node, :primary, host: 'primary-geo-node') }
let!(:primary_node) { create(:geo_node, :primary) }
it 'raises exception when clone_url_prefix is nil' do
allow_any_instance_of(GeoNode).to receive(:clone_url_prefix) { nil }
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe Geo::RepositorySyncService do
include ::EE::GeoHelpers
set(:primary) { create(:geo_node, :primary, host: 'primary-geo-node', relative_url_root: '/gitlab') }
set(:primary) { create(:geo_node, :primary) }
set(:secondary) { create(:geo_node) }
let(:lease) { double(try_obtain: true) }
......@@ -19,7 +19,7 @@ describe Geo::RepositorySyncService do
describe '#execute' do
let(:project) { create(:project_empty_repo) }
let(:repository) { project.repository }
let(:url_to_repo) { "#{primary.url}/#{project.full_path}.git" }
let(:url_to_repo) { "#{primary.url}#{project.full_path}.git" }
before do
allow(Gitlab::ExclusiveLease).to receive(:new)
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe Geo::WikiSyncService do
include ::EE::GeoHelpers
set(:primary) { create(:geo_node, :primary, host: 'primary-geo-node', relative_url_root: '/gitlab') }
set(:primary) { create(:geo_node, :primary) }
set(:secondary) { create(:geo_node) }
let(:lease) { double(try_obtain: true) }
......@@ -19,7 +19,7 @@ RSpec.describe Geo::WikiSyncService do
describe '#execute' do
let(:project) { create(:project_empty_repo) }
let(:repository) { project.wiki.repository }
let(:url_to_repo) { "#{primary.url}/#{project.full_path}.wiki.git" }
let(:url_to_repo) { "#{primary.url}#{project.full_path}.wiki.git" }
before do
allow(Gitlab::ExclusiveLease).to receive(:new)
......
......@@ -20,7 +20,7 @@ describe 'geo rake tasks' do
node = GeoNode.first
expect(node.schema).to eq('https')
expect(node.uri.scheme).to eq('https')
expect(node.primary).to be_truthy
expect(node.geo_node_key).to be_nil
end
......
......@@ -4,7 +4,7 @@ describe Geo::PruneEventLogWorker, :geo do
include ::EE::GeoHelpers
subject(:worker) { described_class.new }
set(:primary) { create(:geo_node, :primary, host: 'primary-geo-node') }
set(:primary) { create(:geo_node, :primary) }
set(:secondary) { create(:geo_node) }
before do
......
......@@ -5,8 +5,9 @@ require 'spec_helper'
describe Geo::RepositorySyncWorker, :geo, :truncate do
include ::EE::GeoHelpers
let(:secondary) { create(:geo_node) }
let(:synced_group) { create(:group) }
let!(:primary) { create(:geo_node, :primary) }
let!(:secondary) { create(:geo_node) }
let!(:synced_group) { create(:group) }
let!(:project_in_synced_group) { create(:project, group: synced_group) }
let!(:unsynced_project) { create(:project) }
......
......@@ -1288,13 +1288,13 @@ cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
clipboard@^1.5.5, clipboard@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53"
clipboard@^1.5.5, clipboard@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b"
dependencies:
good-listener "^1.2.0"
good-listener "^1.2.2"
select "^1.1.2"
tiny-emitter "^1.0.0"
tiny-emitter "^2.0.0"
cliui@^2.1.0:
version "2.1.0"
......@@ -2883,7 +2883,7 @@ globby@^6.1.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
good-listener@^1.2.0:
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
dependencies:
......@@ -6115,9 +6115,9 @@ timers-browserify@^2.0.2:
dependencies:
setimmediate "^1.0.4"
tiny-emitter@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb"
tiny-emitter@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
tmp@0.0.31, tmp@0.0.x:
version "0.0.31"
......
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