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 ...@@ -5,6 +5,7 @@ v 8.7.0
- Refactor group sync to pull access level logic to its own class. !306 - Refactor group sync to pull access level logic to its own class. !306
- [Elastic] Stabilize database indexer if database is inconsistent - [Elastic] Stabilize database indexer if database is inconsistent
- Add ability to sync to remote mirrors. !249 - Add ability to sync to remote mirrors. !249
- GitLab Geo: Many replication improvements and fixes !354
v 8.6.7 v 8.6.7
- No EE-specific changes - No EE-specific changes
......
class Admin::GeoNodesController < Admin::ApplicationController class Admin::GeoNodesController < Admin::ApplicationController
before_action :check_license
def index def index
@nodes = GeoNode.all @nodes = GeoNode.all
@node = GeoNode.new @node = GeoNode.new
...@@ -41,4 +43,11 @@ class Admin::GeoNodesController < Admin::ApplicationController ...@@ -41,4 +43,11 @@ class Admin::GeoNodesController < Admin::ApplicationController
def geo_node_params def geo_node_params
params.require(:geo_node).permit(:url, :primary, geo_node_key_attributes: [:key]) params.require(:geo_node).permit(:url, :primary, geo_node_key_attributes: [:key])
end 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 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 ...@@ -17,9 +17,6 @@ module Projects
project.team.truncate 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_ # Flush the cache for both repositories. This has to be done _before_
# removing the physical repositories as some expiration code depends on # removing the physical repositories as some expiration code depends on
# Git data (e.g. a list of branch names). # Git data (e.g. a list of branch names).
...@@ -27,14 +24,7 @@ module Projects ...@@ -27,14 +24,7 @@ module Projects
Project.transaction do Project.transaction do
project.destroy! project.destroy!
trash_repositories!
unless remove_repository(repo_path)
raise_error('Failed to remove project repository. Please try again or contact administrator')
end
unless remove_repository(wiki_path)
raise_error('Failed to remove wiki repository. Please try again or contact administrator')
end
end end
log_info("Project \"#{project.name}\" was removed") log_info("Project \"#{project.name}\" was removed")
...@@ -42,8 +32,39 @@ module Projects ...@@ -42,8 +32,39 @@ module Projects
true true
end 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 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
unless remove_repository(wiki_path)
raise_error('Failed to remove wiki repository. Please try again or contact administrator')
end
end
def remove_repository(path) def remove_repository(path)
# Skip repository removal. We use this flag when remove user or group # Skip repository removal. We use this flag when remove user or group
return true if params[:skip_repo] == true 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 ...@@ -2,6 +2,7 @@ class HistoricalDataWorker
include Sidekiq::Worker include Sidekiq::Worker
def perform def perform
return if Gitlab::Geo.secondary?
HistoricalData.track! HistoricalData.track!
end end
end end
...@@ -4,6 +4,7 @@ class StuckCiBuildsWorker ...@@ -4,6 +4,7 @@ class StuckCiBuildsWorker
BUILD_STUCK_TIMEOUT = 1.day BUILD_STUCK_TIMEOUT = 1.day
def perform def perform
return if Gitlab::Geo.secondary?
Rails.logger.info 'Cleaning stuck builds' Rails.logger.info 'Cleaning stuck builds'
builds = Ci::Build.running_or_pending.where('updated_at < ?', BUILD_STUCK_TIMEOUT.ago) 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: ...@@ -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 1. Install GitLab Enterprise Edition on the server that will serve as the
secondary Geo node 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 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, After you set up the database replication and configure the GitLab Geo nodes,
there are a few things to consider: there are a few things to consider:
......
...@@ -14,7 +14,8 @@ complete the process. ...@@ -14,7 +14,8 @@ complete the process.
- [Create SSH key pairs for Geo nodes](#create-ssh-key-pairs-for-geo-nodes) - [Create SSH key pairs for Geo nodes](#create-ssh-key-pairs-for-geo-nodes)
- [Primary Node GitLab setup](#primary-node-gitlab-setup) - [Primary Node GitLab setup](#primary-node-gitlab-setup)
- [Secondary Node GitLab setup](#secondary-node-gitlab-setup) - [Secondary Node GitLab setup](#secondary-node-gitlab-setup)
- [Authorized keys regeneration](#authorized-keys-regeneration) - [Database Encryptation Key](#database-encryptation-key)
- [Authorized keys regeneration](#authorized-keys-regeneration)
<!-- END doctoc generated TOC please keep comment here to allow auto update --> <!-- 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 ...@@ -81,8 +82,8 @@ Host example.com # The FQDN of the primary Geo node
## Primary Node GitLab setup ## Primary Node GitLab setup
>**Note:** >**Note:**
You will need to setup your database into a **Master <-> Slave** replication 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 Master 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). 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 Go to the server that you chose to be your primary, and visit
...@@ -119,6 +120,18 @@ Edition installation, with some extra requirements: ...@@ -119,6 +120,18 @@ Edition installation, with some extra requirements:
- Your secondary node should be allowed to communicate via HTTP/HTTPS and - 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). 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 ### Authorized keys regeneration
The final step will be to regenerate the keys for `.ssh/authorized_keys` using 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 ...@@ -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 replicate your GitLab database into another server. You may have to change
some values according to your database setup, how big it is, etc. 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`, The GitLab primary node where the write operations happen will connect to
and the secondary ones which are read-only will act as `slaves`. `primary` database server, and the secondary ones which are read-only will
connect to `secondary` database servers (which are read-only too).
>**Note:** >**Note:**
To be on par with GitLab's notation, we will use `primary` to denote the `master` In many databases documentation you will see `primary` being references as `master`
server, and `secondary` for the `slave`. and `secondary` as either `slave` or `standby` server (read-only).
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
......
...@@ -30,6 +30,15 @@ module API ...@@ -30,6 +30,15 @@ module API
when 'tag_push' when 'tag_push'
required_attributes! %w(event_name project_id project) required_attributes! %w(event_name project_id project)
::Geo::ScheduleWikiRepoUpdateService.new(params).execute ::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 end
end end
......
...@@ -22,6 +22,10 @@ module Gitlab ...@@ -22,6 +22,10 @@ module Gitlab
RequestStore.store[:geo_node_enabled] ||= GeoNode.exists? RequestStore.store[:geo_node_enabled] ||= GeoNode.exists?
end end
def self.license_allows?
::License.current && ::License.current.add_on?('GitLab_Geo')
end
def self.primary? def self.primary?
RequestStore.store[:geo_node_primary?] ||= self.enabled? && self.current_node && self.current_node.primary? RequestStore.store[:geo_node_primary?] ||= self.enabled? && self.current_node && self.current_node.primary?
end end
......
...@@ -70,6 +70,10 @@ module Gitlab ...@@ -70,6 +70,10 @@ module Gitlab
return build_status_object(false, 'The project you were looking for could not be found.') return build_status_object(false, 'The project you were looking for could not be found.')
end 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 case cmd
when *DOWNLOAD_COMMANDS when *DOWNLOAD_COMMANDS
download_access_check download_access_check
...@@ -94,7 +98,7 @@ module Gitlab ...@@ -94,7 +98,7 @@ module Gitlab
def push_access_check(changes) 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.") return build_status_object(false, "You can't push code on a secondary GitLab Geo node.")
end end
......
...@@ -13,8 +13,8 @@ module Gitlab ...@@ -13,8 +13,8 @@ module Gitlab
@env = env @env = env
if disallowed_request? && Gitlab::Geo.secondary? if disallowed_request? && Gitlab::Geo.secondary?
Rails.logger.debug('Gitlab Geo: preventing possible non readonly operation') Rails.logger.debug('GitLab Geo: preventing possible non readonly operation')
error_message = 'You cannot do writing operations on a secondary Gitlab Geo instance' error_message = 'You cannot do writing operations on a secondary GitLab Geo instance'
if json_request? if json_request?
return [403, { 'Content-Type' => 'application/json' }, [{ 'message' => error_message }.to_json]] return [403, { 'Content-Type' => 'application/json' }, [{ 'message' => error_message }.to_json]]
...@@ -60,12 +60,20 @@ module Gitlab ...@@ -60,12 +60,20 @@ module Gitlab
end end
def whitelisted_routes 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 end
def logout_route def logout_route
route_hash[:controller] == 'sessions' && route_hash[:action] == 'destroy' route_hash[:controller] == 'sessions' && route_hash[:action] == 'destroy'
end 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 end
end end
...@@ -68,4 +68,21 @@ describe Gitlab::Geo, lib: true do ...@@ -68,4 +68,21 @@ describe Gitlab::Geo, lib: true do
expect(described_class.geo_node?(host: 'inexistent', port: 1234)).to be_falsey expect(described_class.geo_node?(host: 'inexistent', port: 1234)).to be_falsey
end end
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 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