Commit 97e7080d authored by Gabriel Mazetto's avatar Gabriel Mazetto

Merge branch 'feature/geo-improvements' into 'master'

Geo: Improvements and fixes after QA

We are going to move Geo (#76) to General Availability. This MR will handle all small fixes and changes to get a solid release

- [x] Replicates repository removal (using system hooks)
- [x] Replicates repository rename (using system hooks)
- [x] Replicates repository transfer (using system hooks)
- [x] Whitelisted Sidekiq routes so you can use web interface in a secondary node.
- [x] Change "Gitlab -> GitLab" for the readonly error flash
- [x] Check license add-on to allow any Geo features. 
- [x] Do not execute on a secondary node: `StuckCiBuildsWorker`
- [x] Do not execute on a secondary node: `HistoricalDataWorker`
- [x] Documentation changes: (master, slave -> primary, secondary)
- [x] Document we require `db_key_base`(secrets.yml) to be the same in all nodes

See merge request !354
parents 4bc8c587 630502a3
......@@ -5,6 +5,7 @@ v 8.7.0
- Refactor group sync to pull access level logic to its own class. !306
- [Elastic] Stabilize database indexer if database is inconsistent
- Add ability to sync to remote mirrors. !249
- GitLab Geo: Many replication improvements and fixes !354
v 8.6.7
- No EE-specific changes
......
class Admin::GeoNodesController < Admin::ApplicationController
before_action :check_license
def index
@nodes = GeoNode.all
@node = GeoNode.new
......@@ -41,4 +43,11 @@ class Admin::GeoNodesController < Admin::ApplicationController
def geo_node_params
params.require(:geo_node).permit(:url, :primary, geo_node_key_attributes: [:key])
end
def check_license
unless Gitlab::Geo.license_allows?
flash[:alert] = 'You need a diferent license to enable Geo replication'
redirect_to admin_license_path
end
end
end
module Geo
class MoveRepositoryService
include Gitlab::ShellAdapter
attr_reader :id, :name, :old_path_with_namespace, :new_path_with_namespace
def initialize(id, name, old_path_with_namespace, new_path_with_namespace)
@id = id
@name = name
@old_path_with_namespace = old_path_with_namespace
@new_path_with_namespace = new_path_with_namespace
end
def execute
project = Project.find(id)
project.expire_caches_before_rename(old_path_with_namespace)
# Make sure target directory exists (used when transfering repositories)
project.namespace.ensure_dir_exist
if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace)
# If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository
# So we basically we mute exceptions in next actions
begin
gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
rescue
# Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks
false
end
else
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
raise Exception.new('repository cannot be renamed')
end
end
end
end
module Geo
class ScheduleRepoDestroyService
attr_reader :id, :name, :path_with_namespace
def initialize(params)
@id = params['project_id']
@name = params['name']
@path_with_namespace = params['path_with_namespace']
end
def execute
GeoRepositoryDestroyWorker.perform_async(id, name, path_with_namespace)
end
end
end
module Geo
class ScheduleRepoRenameService
attr_reader :id, :name, :old_path_with_namespace, :path_with_namespace
def initialize(params)
@id = params['project_id']
@name = params['name']
@old_path_with_namespace = params['old_path_with_namespace']
@path_with_namespace = params['path_with_namespace']
end
def execute
GeoRepositoryMoveWorker.perform_async(id, name, old_path_with_namespace, path_with_namespace)
end
end
end
......@@ -17,9 +17,6 @@ module Projects
project.team.truncate
repo_path = project.path_with_namespace
wiki_path = repo_path + '.wiki'
# Flush the cache for both repositories. This has to be done _before_
# removing the physical repositories as some expiration code depends on
# Git data (e.g. a list of branch names).
......@@ -27,7 +24,38 @@ module Projects
Project.transaction do
project.destroy!
trash_repositories!
end
log_info("Project \"#{project.name}\" was removed")
system_hook_service.execute_hooks_for(project, :destroy)
true
end
# Removes physical repository in a Geo replicated secondary node
# There is no need to do any database operation as it will be
# replicated by itself.
def geo_replicate
# Flush the cache for both repositories. This has to be done _before_
# removing the physical repositories as some expiration code depends on
# Git data (e.g. a list of branch names).
flush_caches(project, wiki_path)
trash_repositories!
log_info("Project \"#{project.name}\" was removed")
end
private
def repo_path
project.path_with_namespace
end
def wiki_path
repo_path + '.wiki'
end
def trash_repositories!
unless remove_repository(repo_path)
raise_error('Failed to remove project repository. Please try again or contact administrator')
end
......@@ -37,13 +65,6 @@ module Projects
end
end
log_info("Project \"#{project.name}\" was removed")
system_hook_service.execute_hooks_for(project, :destroy)
true
end
private
def remove_repository(path)
# Skip repository removal. We use this flag when remove user or group
return true if params[:skip_repo] == true
......
class GeoRepositoryDestroyWorker
include Sidekiq::Worker
sidekiq_options queue: :default
def perform(id, name, path_with_namespace)
# We don't have access to the original model anymore, so we are
# rebuilding only what our service class requires
project = FakeProject.new(id, name, path_with_namespace)
::Projects::DestroyService.new(project, nil).geo_replicate
end
FakeProject = Struct.new(:id, :name, :path_with_namespace) do
def repository
@repository ||= Repository.new(path_with_namespace, self)
end
end
end
class GeoRepositoryMoveWorker
include Sidekiq::Worker
sidekiq_options queue: :default
def perform(id, name, old_path_with_namespace, new_path_with_namespace)
Geo::MoveRepositoryService.new(id, name, old_path_with_namespace, new_path_with_namespace).execute
end
end
......@@ -2,6 +2,7 @@ class HistoricalDataWorker
include Sidekiq::Worker
def perform
return if Gitlab::Geo.secondary?
HistoricalData.track!
end
end
......@@ -4,6 +4,7 @@ class StuckCiBuildsWorker
BUILD_STUCK_TIMEOUT = 1.day
def perform
return if Gitlab::Geo.secondary?
Rails.logger.info 'Cleaning stuck builds'
builds = Ci::Build.running_or_pending.where('updated_at < ?', BUILD_STUCK_TIMEOUT.ago)
......
......@@ -54,7 +54,7 @@ Geo instances. Follow the steps below in the order that they appear:
1. Install GitLab Enterprise Edition on the server that will serve as the
secondary Geo node
1. [Setup a database replication](./database.md) in `master <-> slave` topology
1. [Setup a database replication](./database.md) in `primary <-> secondary (read-only)` topology
1. [Configure GitLab](configuration.md) and set the primary and secondary nodes
After you set up the database replication and configure the GitLab Geo nodes,
there are a few things to consider:
......
......@@ -14,6 +14,7 @@ complete the process.
- [Create SSH key pairs for Geo nodes](#create-ssh-key-pairs-for-geo-nodes)
- [Primary Node GitLab setup](#primary-node-gitlab-setup)
- [Secondary Node GitLab setup](#secondary-node-gitlab-setup)
- [Database Encryptation Key](#database-encryptation-key)
- [Authorized keys regeneration](#authorized-keys-regeneration)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
......@@ -81,8 +82,8 @@ Host example.com # The FQDN of the primary Geo node
## Primary Node GitLab setup
>**Note:**
You will need to setup your database into a **Master <-> Slave** replication
topology, and your Primary node should always point to a database's Master
You will need to setup your database into a **Primary <-> Secondary (read-only)** replication
topology, and your Primary node should always point to a database's Primary
instance. If you haven't done that already, read [database replication](./database.md).
Go to the server that you chose to be your primary, and visit
......@@ -119,6 +120,18 @@ Edition installation, with some extra requirements:
- Your secondary node should be allowed to communicate via HTTP/HTTPS and
SSH with your primary node (make sure your firewall is not blocking that).
### Database Encryption Key
GitLab stores a unique encryption key in disk that we use to safely store sensitive
data in the database.
Any secondary node must have the exact same value for `db_key_base` as defined in the primary one.
For Omnibus installations it is stored at `/etc/gitlab/gitlab-secrets.json`.
For Source installations it is stored at `/home/git/gitlab/config/secrets.yml`.
### Authorized keys regeneration
The final step will be to regenerate the keys for `.ssh/authorized_keys` using
......
......@@ -4,12 +4,13 @@ This document describes the minimal steps you have to take in order to
replicate your GitLab database into another server. You may have to change
some values according to your database setup, how big it is, etc.
The GitLab primary node where the write operations happen will act as `master`,
and the secondary ones which are read-only will act as `slaves`.
The GitLab primary node where the write operations happen will connect to
`primary` database server, and the secondary ones which are read-only will
connect to `secondary` database servers (which are read-only too).
>**Note:**
To be on par with GitLab's notation, we will use `primary` to denote the `master`
server, and `secondary` for the `slave`.
In many databases documentation you will see `primary` being references as `master`
and `secondary` as either `slave` or `standby` server (read-only).
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
......
......@@ -30,6 +30,15 @@ module API
when 'tag_push'
required_attributes! %w(event_name project_id project)
::Geo::ScheduleWikiRepoUpdateService.new(params).execute
when 'project_destroy'
required_attributes! %w(event_name project_id path_with_namespace)
::Geo::ScheduleRepoDestroyService.new(params).execute
when 'project_rename'
required_attributes! %w(event_name project_id path_with_namespace old_path_with_namespace)
::Geo::ScheduleRepoRenameService.new(params).execute
when 'project_transfer'
required_attributes! %w(event_name project_id path_with_namespace old_path_with_namespace)
::Geo::ScheduleRepoRenameService.new(params).execute
end
end
end
......
......@@ -22,6 +22,10 @@ module Gitlab
RequestStore.store[:geo_node_enabled] ||= GeoNode.exists?
end
def self.license_allows?
::License.current && ::License.current.add_on?('GitLab_Geo')
end
def self.primary?
RequestStore.store[:geo_node_primary?] ||= self.enabled? && self.current_node && self.current_node.primary?
end
......
......@@ -70,6 +70,10 @@ module Gitlab
return build_status_object(false, 'The project you were looking for could not be found.')
end
if Gitlab::Geo.secondary? && !Gitlab::Geo.license_allows?
return build_status_object(false, 'Your current license does not have GitLab Geo add-on enabled.')
end
case cmd
when *DOWNLOAD_COMMANDS
download_access_check
......@@ -94,7 +98,7 @@ module Gitlab
def push_access_check(changes)
if Gitlab::Geo.enabled? && Gitlab::Geo.secondary?
if Gitlab::Geo.secondary?
return build_status_object(false, "You can't push code on a secondary GitLab Geo node.")
end
......
......@@ -13,8 +13,8 @@ module Gitlab
@env = env
if disallowed_request? && Gitlab::Geo.secondary?
Rails.logger.debug('Gitlab Geo: preventing possible non readonly operation')
error_message = 'You cannot do writing operations on a secondary Gitlab Geo instance'
Rails.logger.debug('GitLab Geo: preventing possible non readonly operation')
error_message = 'You cannot do writing operations on a secondary GitLab Geo instance'
if json_request?
return [403, { 'Content-Type' => 'application/json' }, [{ 'message' => error_message }.to_json]]
......@@ -60,12 +60,20 @@ module Gitlab
end
def whitelisted_routes
logout_route || WHITELISTED.any? { |path| @request.path.include?(path) }
logout_route || grack_route || WHITELISTED.any? { |path| @request.path.include?(path) } || sidekiq_route
end
def logout_route
route_hash[:controller] == 'sessions' && route_hash[:action] == 'destroy'
end
def sidekiq_route
@request.path.start_with?('/admin/sidekiq')
end
def grack_route
@request.path.end_with?('.git/git-upload-pack')
end
end
end
end
......@@ -68,4 +68,21 @@ describe Gitlab::Geo, lib: true 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
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Geo') { true }
expect(described_class.license_allows?).to be_truthy
end
it 'returns false if license doesnt have Geo addon' do
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_Geo') { false }
expect(described_class.license_allows?).to be_falsey
end
it 'returns false if no license is present' do
allow(License).to receive(:current) { nil }
expect(described_class.license_allows?).to be_falsey
end
end
end
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