Commit e6cdc9ad authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'upstream/master' into sh-geo-fix-snippet-downloads

parents 37b623b0 1d5b5df0
......@@ -3,18 +3,23 @@
# Automatically sets the layout and ensures an administrator is logged in
class Admin::ApplicationController < ApplicationController
before_action :authenticate_admin!
before_action :display_geo_information
before_action :display_read_only_information
layout 'admin'
def authenticate_admin!
render_404 unless current_user.admin?
end
def display_geo_information
return unless Gitlab::Geo.secondary?
return unless Gitlab::Geo.primary_node_configured?
def display_read_only_information
return unless Gitlab::Database.read_only?
primary_node = view_context.link_to('primary node', Gitlab::Geo.primary_node.url)
flash.now[:notice] = "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the #{primary_node}.".html_safe
flash.now[:notice] = read_only_message
end
private
# Overridden in EE
def read_only_message
_('You are on a read-only GitLab instance.')
end
end
......@@ -12,7 +12,7 @@ module Boards
def index
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20)
make_sure_position_is_set(issues) unless Gitlab::Geo.secondary?
make_sure_position_is_set(issues) if Gitlab::Database.read_write?
issues = issues.preload(:project,
:milestone,
:assignees,
......
......@@ -4,6 +4,8 @@ class Projects::LfsApiController < Projects::GitHttpClientController
include GitlabRoutingHelper
include LfsRequest
prepend ::EE::Projects::LfsApiController
skip_before_action :lfs_check_access!, only: [:deprecated]
before_action :lfs_check_batch_operation!, only: [:batch]
......@@ -96,14 +98,19 @@ class Projects::LfsApiController < Projects::GitHttpClientController
end
def lfs_check_batch_operation!
if upload_request? && Gitlab::Geo.secondary?
if upload_request? && Gitlab::Database.read_only?
render(
json: {
message: "You cannot write to a secondary GitLab Geo instance. Please use #{geo_primary_default_url_to_repo(project)} instead."
message: lfs_read_only_message
},
content_type: "application/vnd.git-lfs+json",
content_type: 'application/vnd.git-lfs+json',
status: 403
)
end
end
# Overridden in EE
def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.')
end
end
......@@ -15,7 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
# Make sure merge requests created before 8.0
# have head file in refs/merge-requests/
def ensure_ref_fetched
@merge_request.ensure_ref_fetched
@merge_request.ensure_ref_fetched if Gitlab::Database.read_write?
end
def merge_request_params
......
......@@ -9,9 +9,7 @@ class SessionsController < Devise::SessionsController
prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create]
prepend_before_action :store_redirect_path, only: [:new]
before_action :gitlab_geo_login, only: [:new]
before_action :gitlab_geo_logout, only: [:destroy]
prepend_before_action :store_redirect_uri, only: [:new]
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
......@@ -88,7 +86,11 @@ class SessionsController < Devise::SessionsController
end
end
def store_redirect_path
def stored_redirect_uri
@redirect_to ||= stored_location_for(:redirect)
end
def store_redirect_uri
redirect_uri =
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
URI(request.referer)
......@@ -98,40 +100,22 @@ class SessionsController < Devise::SessionsController
# Prevent a 'you are already signed in' message directly after signing:
# we should never redirect to '/users/sign_in' after signing in successfully.
if redirect_uri.path == new_user_session_path
return true
elsif redirect_uri.host == Gitlab.config.gitlab.host && redirect_uri.port == Gitlab.config.gitlab.port
redirect_to = redirect_uri.to_s
elsif Gitlab::Geo.geo_node?(host: redirect_uri.host, port: redirect_uri.port)
redirect_to = redirect_uri.to_s
end
return true if redirect_uri.path == new_user_session_path
redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri)
@redirect_to = redirect_to
store_location_for(:redirect, redirect_to)
end
def two_factor_enabled?
find_user.try(:two_factor_enabled?)
end
def gitlab_geo_login
return unless Gitlab::Geo.secondary?
return if signed_in?
oauth = Gitlab::Geo::OauthSession.new
# share full url with primary node by oauth state
user_return_to = URI.join(root_url, session[:user_return_to].to_s).to_s
oauth.return_to = @redirect_to || user_return_to
redirect_to oauth_geo_auth_url(state: oauth.generate_oauth_state)
# Overridden in EE
def redirect_allowed_to?(uri)
uri.host == Gitlab.config.gitlab.host &&
uri.port == Gitlab.config.gitlab.port
end
def gitlab_geo_logout
return unless Gitlab::Geo.secondary?
oauth = Gitlab::Geo::OauthSession.new(access_token: session[:access_token])
@geo_logout_state = oauth.generate_logout_state
def two_factor_enabled?
find_user&.two_factor_enabled?
end
def auto_sign_in_with_provider
......
......@@ -59,7 +59,7 @@ module CacheMarkdownField
# Update every column in a row if any one is invalidated, as we only store
# one version per row
def refresh_markdown_cache!(do_update: false)
def refresh_markdown_cache
options = { skip_project_check: skip_project_check? }
updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
......@@ -71,8 +71,14 @@ module CacheMarkdownField
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
updates.each {|html_field, data| write_attribute(html_field, data) }
end
def refresh_markdown_cache!
updates = refresh_markdown_cache
return unless persisted? && Gitlab::Database.read_write?
update_columns(updates) if persisted? && do_update
update_columns(updates)
end
def cached_html_up_to_date?(markdown_field)
......@@ -124,8 +130,8 @@ module CacheMarkdownField
end
# Using before_update here conflicts with elasticsearch-model somehow
before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache?
before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache?
before_create :refresh_markdown_cache, if: :invalidated_markdown_cache?
before_update :refresh_markdown_cache, if: :invalidated_markdown_cache?
end
class_methods do
......
......@@ -156,7 +156,7 @@ module Routable
end
def update_route
return if Gitlab::Geo.secondary?
return if Gitlab::Database.read_only?
prepare_route
route.save
......
......@@ -43,15 +43,17 @@ module TokenAuthenticatable
write_attribute(token_field, token) if token
end
# Returns a token, but only saves when the database is in read & write mode
define_method("ensure_#{token_field}!") do
send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend
read_attribute(token_field)
end
# Resets the token, but only saves when the database is in read & write mode
define_method("reset_#{token_field}!") do
write_new_token(token_field)
save!
save! if Gitlab::Database.read_write?
end
end
end
......
......@@ -498,7 +498,7 @@ class MergeRequest < ActiveRecord::Base
end
def check_if_can_be_merged
return unless unchecked? && !Gitlab::Geo.secondary?
return unless unchecked? && Gitlab::Database.read_write?
can_be_merged =
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
......
......@@ -811,7 +811,7 @@ class Project < ActiveRecord::Base
end
def cache_has_external_issue_tracker
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
end
def has_wiki?
......@@ -831,7 +831,7 @@ class Project < ActiveRecord::Base
end
def cache_has_external_wiki
update_column(:has_external_wiki, services.external_wikis.any?)
update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
end
def find_or_initialize_services(exceptions: [])
......
......@@ -477,6 +477,14 @@ class User < ActiveRecord::Base
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
def remember_me!
super if ::Gitlab::Database.read_write?
end
def forget_me!
super if ::Gitlab::Database.read_write?
end
def disable_two_factor!
transaction do
update_attributes(
......
module Keys
class LastUsedService
prepend ::EE::Keys::LastUsedService
TIMEOUT = 1.day.to_i
attr_reader :key
......@@ -18,6 +16,8 @@ module Keys
end
def update?
return false if ::Gitlab::Database.read_only?
last_used = key.last_used_at
return false if last_used && (Time.zone.now - last_used) <= TIMEOUT
......
......@@ -14,7 +14,7 @@ module Users
private
def record_activity
Gitlab::UserActivities.record(@author.id) unless Gitlab::Geo.secondary?
Gitlab::UserActivities.record(@author.id) if Gitlab::Database.read_write?
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})")
end
......
......@@ -84,7 +84,7 @@
%p
.js-health
- unless Gitlab::Geo.secondary?
- if Gitlab::Database.read_write?
.node-actions
- if Gitlab::Geo.license_allows?
- if node.missing_oauth_application?
......
module ProjectStartImport
def start(project)
if project.import_started? && project.import_jid == self.jid
return true
end
project.import_start
end
end
......@@ -4,6 +4,7 @@ class RepositoryForkWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
include ProjectStartImport
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
......@@ -37,7 +38,7 @@ class RepositoryForkWorker
private
def start_fork(project)
return true if project.import_start
return true if start(project)
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")
false
......
......@@ -4,6 +4,7 @@ class RepositoryImportWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
include ExceptionBacktrace
include ProjectStartImport
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
......@@ -38,7 +39,7 @@ class RepositoryImportWorker
private
def start_import(project)
return true if project.import_start
return true if start(project)
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false
......
......@@ -4,6 +4,7 @@ class RepositoryUpdateMirrorWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
include ProjectStartImport
LEASE_KEY = 'repository_update_mirror_worker_start_scheduler'.freeze
LEASE_TIMEOUT = 2.seconds
......@@ -45,7 +46,7 @@ class RepositoryUpdateMirrorWorker
end
def start_mirror(project)
if project.import_start
if start(project)
Rails.logger.info("Mirror update for #{project.full_path} started. Waiting duration: #{project.mirror_waiting_duration}")
Gitlab::Metrics.add_event_with_values(
:mirrors_running,
......
---
title: Schedule repository synchronization when processing events on a Geo secondary
node
merge_request: 2838
author:
type: changed
---
title: Create idea of read-only database and add method to check for it
merge_request: 2954
author:
type: changed
......@@ -174,8 +174,8 @@ module Gitlab
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
ENV['GIT_TERMINAL_PROMPT'] = '0'
# Gitlab Geo Middleware support
config.middleware.insert_after ActionDispatch::Flash, 'Gitlab::Middleware::ReadonlyGeo'
# Gitlab Read-only middleware support
config.middleware.insert_after ActionDispatch::Flash, 'Gitlab::Middleware::ReadOnly'
config.generators do |g|
g.factory_girl false
......
......@@ -395,13 +395,12 @@ your installation compares before proceeding.
There are two encryption methods, `simple_tls` and `start_tls`.
For either encryption method, if setting `validate_certificates: false`, TLS
For either encryption method, if setting `verify_certificates: false`, TLS
encryption is established with the LDAP server before any LDAP-protocol data is
exchanged but no validation of the LDAP server's SSL certificate is performed.
>**Note**: Before GitLab 9.5, `validate_certificates: false` is the default if
>**Note**: Before GitLab 9.5, `verify_certificates: false` is the default if
unspecified.
>>>>>>> upstream/master
## Limitations
......
......@@ -78,19 +78,20 @@ to see if there are changes since the last time the log was checked
and will handle repository updates, deletes, changes & renames.
## Readonly
## Read-only
All **Secondary** nodes are read-only.
We have a Rails Middleware that filters any potentially writing operations
and prevent user from trying to update the database and getting a 500 error
(see `Gitlab::Middleware::ReadonlyGeo`).
The general principle of a [read-only database](verifying_database_capabilities.md#read-only-database)
applies to all Geo secondary nodes. So `Gitlab::Database.read_only?`
will always return `true` on a secondary node.
Database will already be read-only in a replicated setup, so we don't need to
take any extra step for that.
When some write actions are not allowed, because the node is a
secondary, consider the `Gitlab::Database.read_only?` or `Gitlab::Database.read_write?`
guard, instead of `Gitlab::Geo.secondary?`.
We do use our feature toggle `.secondary?` to coordinate Git operations and do
the correct authorization (denying writing on any secondary node).
Database itself will already be read-only in a replicated setup, so we
don't need to take any extra step for that.
## File Transfers
......
......@@ -24,3 +24,15 @@ else
run_query
end
```
# Read-only database
The database can be used in read-only mode. In this case we have to
make sure all GET requests don't attempt any write operations to the
database. If one of those requests wants to write to the database, it needs
to be wrapped in a `Gitlab::Database.read_only?` or `Gitlab::Database.read_write?`
guard, to make sure it doesn't for read-only databases.
We have a Rails Middleware that filters any potentially writing
operations (the CUD operations of CRUD) and prevent the user from trying
to update the database and getting a 500 error (see `Gitlab::Middleware::ReadOnly`).
module EE
module Admin
module ApplicationController
def read_only_message
raise NotImplementedError unless defined?(super)
return super unless Gitlab::Geo.secondary_with_primary?
link_to_primary_node = view_context.link_to('primary node', Gitlab::Geo.primary_node.url)
(_('You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}.') % { link_to_primary_node: link_to_primary_node }).html_safe
end
end
end
end
module EE
module Projects
module LfsApiController
def lfs_read_only_message
raise NotImplementedError unless defined?(super)
return super unless ::Gitlab::Geo.secondary_with_primary?
(_('You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead.') % { link_to_primary_node: geo_primary_default_url_to_repo(project) }).html_safe
end
end
end
end
......@@ -2,13 +2,45 @@ module EE
module SessionsController
extend ActiveSupport::Concern
prepended do
before_action :gitlab_geo_login, only: [:new]
before_action :gitlab_geo_logout, only: [:destroy]
end
private
def gitlab_geo_login
return unless ::Gitlab::Geo.secondary?
return if signed_in?
oauth = ::Gitlab::Geo::OauthSession.new
# share full url with primary node by oauth state
user_return_to = URI.join(root_url, session[:user_return_to].to_s).to_s
oauth.return_to = stored_redirect_uri || user_return_to
redirect_to oauth_geo_auth_url(state: oauth.generate_oauth_state)
end
def gitlab_geo_logout
return unless ::Gitlab::Geo.secondary?
oauth = ::Gitlab::Geo::OauthSession.new(access_token: session[:access_token])
@geo_logout_state = oauth.generate_logout_state
end
def log_failed_login
::AuditEventService.new(request.filtered_parameters['user']['login'], nil, ip_address: request.remote_ip)
.for_failed_login.unauth_security_event
super
end
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)
end
end
end
......@@ -273,14 +273,6 @@ module EE
.order(order % quoted_values) # `order` cannot escape for us!
end
def cache_has_external_issue_tracker
super unless ::Gitlab::Geo.secondary?
end
def cache_has_external_wiki
super unless ::Gitlab::Geo.secondary?
end
def execute_hooks(data, hooks_scope = :push_hooks)
super
......
......@@ -84,16 +84,6 @@ module EE
super || auditor?
end
def remember_me!
return if ::Gitlab::Geo.secondary?
super
end
def forget_me!
return if ::Gitlab::Geo.secondary?
super
end
def email_opted_in_source
email_opted_in_source_id == EMAIL_OPT_IN_SOURCE_ID_GITLAB_COM ? 'GitLab.com' : ''
end
......
module EE
module Keys
module LastUsedService
def update?
module Gitlab
module Database
def self.read_only?
raise NotImplementedError unless defined?(super)
!::Gitlab::Geo.secondary? && super
Gitlab::Geo.secondary? || super
end
end
end
......
......@@ -40,7 +40,7 @@ module Banzai
return cacheless_render_field(object, field)
end
object.refresh_markdown_cache!(do_update: update_object?(object)) unless object.cached_html_up_to_date?(field)
object.refresh_markdown_cache! unless object.cached_html_up_to_date?(field)
object.cached_html_for(field)
end
......@@ -162,10 +162,5 @@ module Banzai
return unless cache_key
Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend
end
# GitLab EE needs to disable updates on GET requests in Geo
def self.update_object?(object)
!Gitlab::Geo.secondary?
end
end
end
module Gitlab
module Database
extend ::EE::Gitlab::Database
# The max value of INTEGER type is the same between MySQL and PostgreSQL:
# https://www.postgresql.org/docs/9.2/static/datatype-numeric.html
# http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
......@@ -29,6 +31,15 @@ module Gitlab
adapter_name.casecmp('postgresql').zero?
end
# Overridden in EE
def self.read_only?
false
end
def self.read_write?
!self.read_only?
end
def self.version
database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
......
......@@ -54,6 +54,10 @@ module Gitlab
Gitlab::Geo.primary_node.present?
end
def self.secondary_with_primary?
self.secondary? && self.primary_node_configured?
end
def self.license_allows?
::License.feature_available?(:geo)
end
......
......@@ -65,15 +65,15 @@ module Gitlab
next unless can_replay?(event_log)
if event_log.repository_updated_event
handle_repository_update(event_log)
handle_repository_updated(event_log)
elsif event_log.repository_created_event
handle_repository_created(event_log)
elsif event_log.repository_deleted_event
handle_repository_delete(event_log)
handle_repository_deleted(event_log)
elsif event_log.repositories_changed_event
handle_repositories_changed(event_log.repositories_changed_event)
elsif event_log.repository_renamed_event
handle_repository_rename(event_log)
handle_repository_renamed(event_log)
end
end
end
......@@ -107,93 +107,96 @@ module Gitlab
end
def handle_repository_created(event_log)
created_event = event_log.repository_created_event
registry = ::Geo::ProjectRegistry.find_or_initialize_by(project_id: created_event.project_id)
registry.resync_repository = true
registry.resync_wiki = created_event.wiki_path.present?
event = event_log.repository_created_event
registry = find_or_initialize_registry(event.project_id, resync_repository: true, resync_wiki: event.wiki_path.present?)
log_event_info(
event_log.created_at,
message: 'Repository created',
project_id: created_event.project_id,
repo_path: created_event.repo_path,
wiki_path: created_event.wiki_path,
project_id: event.project_id,
repo_path: event.repo_path,
wiki_path: event.wiki_path,
resync_repository: registry.resync_repository,
resync_wiki: registry.resync_wiki)
registry.save!
end
def handle_repository_update(event)
updated_event = event.repository_updated_event
registry = ::Geo::ProjectRegistry.find_or_initialize_by(project_id: updated_event.project_id)
::Geo::ProjectSyncWorker.perform_async(event.project_id, Time.now)
end
case updated_event.source
when 'repository'
registry.resync_repository = true
when 'wiki'
registry.resync_wiki = true
end
def handle_repository_updated(event_log)
event = event_log.repository_updated_event
registry = find_or_initialize_registry(event.project_id, "resync_#{event.source}" => true)
log_event_info(
event.created_at,
message: "Repository update",
project_id: updated_event.project_id,
source: updated_event.source,
event_log.created_at,
message: 'Repository update',
project_id: event.project_id,
source: event.source,
resync_repository: registry.resync_repository,
resync_wiki: registry.resync_wiki)
registry.save!
::Geo::ProjectSyncWorker.perform_async(event.project_id, Time.now)
end
def handle_repository_delete(event)
deleted_event = event.repository_deleted_event
full_path = File.join(deleted_event.repository_storage_path,
deleted_event.deleted_path)
def handle_repository_deleted(event_log)
event = event_log.repository_deleted_event
disk_path = File.join(event.repository_storage_path, event.deleted_path)
job_id = ::Geo::RepositoryDestroyService
.new(deleted_event.project_id,
deleted_event.deleted_project_name,
full_path,
deleted_event.repository_storage_name)
.new(event.project_id, event.deleted_project_name, disk_path, event.repository_storage_name)
.async_execute
log_event_info(event.created_at,
message: "Deleted project",
project_id: deleted_event.project_id,
full_path: full_path,
job_id: job_id)
log_event_info(
event_log.created_at,
message: 'Deleted project',
project_id: event.project_id,
disk_path: disk_path,
job_id: job_id)
# No need to create a project entry if it doesn't exist
::Geo::ProjectRegistry.where(project_id: deleted_event.project_id).delete_all
::Geo::ProjectRegistry.where(project_id: event.project_id).delete_all
end
def handle_repositories_changed(changed_event)
return unless Gitlab::Geo.current_node.id == changed_event.geo_node_id
def handle_repositories_changed(event)
return unless Gitlab::Geo.current_node.id == event.geo_node_id
job_id = ::Geo::RepositoriesCleanUpWorker.perform_in(1.hour, changed_event.geo_node_id)
job_id = ::Geo::RepositoriesCleanUpWorker.perform_in(1.hour, event.geo_node_id)
if job_id
log_info('Scheduled repositories clean up for Geo node', geo_node_id: changed_event.geo_node_id, job_id: job_id)
log_info('Scheduled repositories clean up for Geo node', geo_node_id: event.geo_node_id, job_id: job_id)
else
log_error('Could not schedule repositories clean up for Geo node', geo_node_id: changed_event.geo_node_id)
log_error('Could not schedule repositories clean up for Geo node', geo_node_id: event.geo_node_id)
end
end
def handle_repository_rename(event)
renamed_event = event.repository_renamed_event
return unless renamed_event.project_id
def handle_repository_renamed(event_log)
event = event_log.repository_renamed_event
return unless event.project_id
old_path = renamed_event.old_path_with_namespace
new_path = renamed_event.new_path_with_namespace
old_path = event.old_path_with_namespace
new_path = event.new_path_with_namespace
job_id = ::Geo::MoveRepositoryService
.new(renamed_event.project_id, "", old_path, new_path)
.new(event.project_id, '', old_path, new_path)
.async_execute
log_event_info(event.created_at,
message: "Renaming project",
project_id: renamed_event.project_id,
old_path: old_path,
new_path: new_path,
job_id: job_id)
log_event_info(
event_log.created_at,
message: 'Renaming project',
project_id: event.project_id,
old_path: old_path,
new_path: new_path,
job_id: job_id)
end
def find_or_initialize_registry(project_id, attrs)
registry = ::Geo::ProjectRegistry.find_or_initialize_by(project_id: project_id)
registry.assign_attributes(attrs)
registry
end
def cursor_delay(created_at)
......
......@@ -19,8 +19,8 @@ module Gitlab
command_not_allowed: "The command you're trying to execute is not allowed.",
upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
readonly: 'The repository is temporarily read-only. Please try again later.',
cannot_push_to_secondary_geo: "You can't push code to a secondary GitLab Geo node."
read_only: 'The repository is temporarily read-only. Please try again later.',
cannot_push_to_read_only: "You can't push code to a read-only GitLab instance."
}.freeze
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
......@@ -173,11 +173,11 @@ module Gitlab
# TODO: please clean this up
def check_push_access!(changes)
if project.repository_read_only?
raise UnauthorizedError, ERROR_MESSAGES[:readonly]
raise UnauthorizedError, ERROR_MESSAGES[:read_only]
end
if Gitlab::Geo.secondary?
raise UnauthorizedError, ERROR_MESSAGES[:cannot_push_to_secondary_geo]
if Gitlab::Database.read_only?
raise UnauthorizedError, ERROR_MESSAGES[:cannot_push_to_read_only]
end
if deploy_key
......
module Gitlab
class GitAccessWiki < GitAccess
ERROR_MESSAGES = {
geo: "You can't push code to a secondary GitLab Geo node.",
read_only: "You can't push code to a read-only GitLab instance.",
write_to_wiki: "You are not allowed to write to this project's wiki."
}.freeze
......@@ -18,8 +18,8 @@ module Gitlab
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end
if Gitlab::Geo.enabled? && Gitlab::Geo.secondary?
raise UnauthorizedError, ERROR_MESSAGES[:geo]
if Gitlab::Database.read_only?
raise UnauthorizedError, ERROR_MESSAGES[:read_only]
end
true
......
module Gitlab
module Middleware
class ReadonlyGeo
class ReadOnly
DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
APPLICATION_JSON = 'application/json'.freeze
API_VERSIONS = (3..4)
......@@ -13,9 +13,9 @@ module Gitlab
def call(env)
@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'
if disallowed_request? && Gitlab::Database.read_only?
Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
error_message = 'You cannot do writing operations on a read-only GitLab instance'
if json_request?
return [403, { 'Content-Type' => 'application/json' }, [{ 'message' => error_message }.to_json]]
......
require Rails.root.join('ee/lib/ee/gitlab/database')
require Rails.root.join('lib/gitlab/database')
require Rails.root.join('lib/gitlab/database/migration_helpers')
require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
......
......@@ -83,8 +83,8 @@ describe EE::User do
expect(subject.reload.remember_created_at).to be_nil
end
it 'does not clear remember_created_at when in a Geo secondary node' do
allow(Gitlab::Geo).to receive(:secondary?) { true }
it 'does not clear remember_created_at when in a GitLab read-only instance' do
allow(Gitlab::Database).to receive(:read_only?) { true }
expect { subject.forget_me! }.not_to change(subject, :remember_created_at)
end
......@@ -99,8 +99,8 @@ describe EE::User do
expect(subject.reload.remember_created_at).not_to be_nil
end
it 'does not update remember_created_at when in a Geo secondary node' do
allow(Gitlab::Geo).to receive(:secondary?) { true }
it 'does not update remember_created_at when in a Geo read-only instance' do
allow(Gitlab::Database).to receive(:read_only?) { true }
expect { subject.remember_me! }.not_to change(subject, :remember_created_at)
end
......
require 'spec_helper'
describe Keys::LastUsedService do
it 'does not run on Geo secondaries', :clean_gitlab_redis_shared_state do
it 'does not run on read-only GitLab instances', :clean_gitlab_redis_shared_state do
key = create(:key, last_used_at: 1.year.ago)
original_time = key.last_used_at
allow(::Gitlab::Geo).to receive(:secondary?).and_return(true)
allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
described_class.new(key).execute
expect(key.reload.last_used_at).to be_like_time(original_time)
......
......@@ -174,7 +174,7 @@ FactoryGirl.define do
end
end
trait :readonly do
trait :read_only do
repository_read_only true
end
......
......@@ -31,14 +31,14 @@ describe Banzai::Renderer do
let(:object) { fake_object(fresh: false) }
it 'caches and returns the result' do
expect(object).to receive(:refresh_markdown_cache!).with(do_update: true)
expect(object).to receive(:refresh_markdown_cache!)
is_expected.to eq('field_html')
end
it "skips database caching on a Geo secondary" do
allow(Gitlab::Geo).to receive(:secondary?).and_return(true)
expect(object).to receive(:refresh_markdown_cache!).with(do_update: false)
it "skips database caching on a GitLab read-only instance" do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
expect(object).to receive(:refresh_markdown_cache!)
is_expected.to eq('field_html')
end
......
......@@ -65,7 +65,9 @@ describe Gitlab::Geo::DatabaseTasks do
describe described_class::Migrate do
describe '.up' do
it 'requires ENV["VERSION"] to be set' do
expect { subject.up }.to raise_error(String)
stub_env('VERSION', nil)
expect { subject.up }.to raise_error(/VERSION is required/)
end
it 'calls ActiveRecord::Migrator.run' do
......@@ -78,7 +80,9 @@ describe Gitlab::Geo::DatabaseTasks do
describe '.down' do
it 'requires ENV["VERSION"] to be set' do
expect { subject.down }.to raise_error(String)
stub_env('VERSION', nil)
expect { subject.down }.to raise_error(/VERSION is required/)
end
it 'calls ActiveRecord::Migrator.run' do
......
......@@ -4,7 +4,7 @@ describe Gitlab::Geo::LogCursor::Daemon, :postgresql do
include ::EE::GeoHelpers
describe '#run!' do
set(:geo_node) { create(:geo_node) }
set(:geo_node) { create(:geo_node, :primary) }
before do
stub_current_geo_node(geo_node)
......@@ -30,7 +30,8 @@ describe Gitlab::Geo::LogCursor::Daemon, :postgresql do
end
context 'when replaying a repository created event' do
let(:repository_created_event) { create(:geo_repository_created_event) }
let(:project) { create(:project) }
let(:repository_created_event) { create(:geo_repository_created_event, project: project) }
let(:event_log) { create(:geo_event_log, repository_created_event: repository_created_event) }
let!(:event_log_state) { create(:geo_event_log_state, event_id: event_log.id - 1) }
......@@ -47,7 +48,7 @@ describe Gitlab::Geo::LogCursor::Daemon, :postgresql do
registry = Geo::ProjectRegistry.last
expect(registry).to have_attributes(resync_repository: true, resync_wiki: true)
expect(registry).to have_attributes(project_id: project.id, resync_repository: true, resync_wiki: true)
end
it 'sets resync_wiki to false if wiki_path is nil' do
......@@ -57,14 +58,22 @@ describe Gitlab::Geo::LogCursor::Daemon, :postgresql do
registry = Geo::ProjectRegistry.last
expect(registry).to have_attributes(resync_repository: true, resync_wiki: false)
expect(registry).to have_attributes(project_id: project.id, resync_repository: true, resync_wiki: false)
end
it 'performs Geo::ProjectSyncWorker' do
expect(Geo::ProjectSyncWorker).to receive(:perform_async)
.with(project.id, anything).once
subject.run!
end
end
context 'when replaying a repository updated event' do
let(:event_log) { create(:geo_event_log, :updated_event) }
let(:project) { create(:project) }
let(:repository_updated_event) { create(:geo_repository_updated_event, project: project) }
let(:event_log) { create(:geo_event_log, repository_updated_event: repository_updated_event) }
let!(:event_log_state) { create(:geo_event_log_state, event_id: event_log.id - 1) }
let(:repository_updated_event) { event_log.repository_updated_event }
before do
allow(subject).to receive(:exit?).and_return(false, true)
......@@ -91,6 +100,13 @@ describe Gitlab::Geo::LogCursor::Daemon, :postgresql do
expect(registry.reload.resync_wiki).to be true
end
it 'performs Geo::ProjectSyncWorker' do
expect(Geo::ProjectSyncWorker).to receive(:perform_async)
.with(project.id, anything).once
subject.run!
end
end
context 'when replaying a repository deleted event' do
......@@ -155,6 +171,7 @@ describe Gitlab::Geo::LogCursor::Daemon, :postgresql do
before do
allow(subject).to receive(:exit?).and_return(false, true)
allow(Geo::ProjectSyncWorker).to receive(:perform_async)
end
it 'replays events for projects that belong to selected namespaces to replicate' do
......
......@@ -110,7 +110,7 @@ describe Gitlab::Geo do
end
end
describe 'readonly?' do
describe 'secondary?' do
context 'when current node is secondary' do
it 'returns true' do
stub_current_geo_node(secondary_node)
......
......@@ -744,11 +744,10 @@ describe Gitlab::GitAccess do
run_permission_checks(admin: matrix)
end
context "when in a secondary gitlab geo node" do
context "when in a read-only GitLab instance" do
before do
create(:protected_branch, name: 'feature', project: project)
allow(Gitlab::Geo).to receive(:enabled?) { true }
allow(Gitlab::Geo).to receive(:secondary?) { true }
allow(Gitlab::Database).to receive(:read_only?) { true }
end
# Only check admin; if an admin can't do it, other roles can't either
......@@ -944,7 +943,7 @@ describe Gitlab::GitAccess do
end
context 'when the repository is read only' do
let(:project) { create(:project, :repository, :readonly) }
let(:project) { create(:project, :repository, :read_only) }
it 'denies push access' do
project.add_master(user)
......
......@@ -25,15 +25,13 @@ describe Gitlab::GitAccessWiki do
it { expect { subject }.not_to raise_error }
context 'when in a secondary gitlab geo node' do
context 'when in a read-only GitLab instance' do
before do
allow(Gitlab::Geo).to receive(:enabled?) { true }
allow(Gitlab::Geo).to receive(:secondary?) { true }
allow(Gitlab::Geo).to receive(:license_allows?) { true }
allow(Gitlab::Database).to receive(:read_only?) { true }
end
it 'does not give access to upload wiki code' do
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "You can't push code to a secondary GitLab Geo node.")
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "You can't push code to a read-only GitLab instance.")
end
end
end
......
require 'spec_helper'
describe Gitlab::Middleware::ReadonlyGeo do
describe Gitlab::Middleware::ReadOnly do
include Rack::Test::Methods
RSpec::Matchers.define :be_a_redirect do
......@@ -38,11 +38,11 @@ describe Gitlab::Middleware::ReadonlyGeo do
let(:request) { Rack::MockRequest.new(rack_stack) }
context 'normal requests to a secondary Gitlab Geo' do
context 'normal requests to a read-only Gitlab instance' do
let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } }
before do
allow(Gitlab::Geo).to receive(:secondary?) { true }
allow(Gitlab::Database).to receive(:read_only?) { true }
end
it 'expects PATCH requests to be disallowed' do
......@@ -98,13 +98,6 @@ describe Gitlab::Middleware::ReadonlyGeo do
expect(subject).not_to disallow_request
end
it 'expects a GET status request to be allowed' do
response = request.get("/api/#{API::API.version}/geo/status")
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
end
it 'expects a POST LFS request to batch URL to be allowed' do
response = request.post('/root/rouge.git/info/lfs/objects/batch')
......@@ -114,12 +107,12 @@ describe Gitlab::Middleware::ReadonlyGeo do
end
end
context 'json requests to a secondary Geo node' do
context 'json requests to a read-only GitLab instance' do
let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'application/json' }, ['OK']] } }
let(:content_json) { { 'CONTENT_TYPE' => 'application/json' } }
before do
allow(Gitlab::Geo).to receive(:secondary?) { true }
allow(Gitlab::Database).to receive(:read_only?) { true }
end
it 'expects PATCH requests to be disallowed' do
......
......@@ -178,57 +178,59 @@ describe CacheMarkdownField do
end
end
describe '#refresh_markdown_cache!' do
describe '#refresh_markdown_cache' do
before do
thing.foo = updated_markdown
end
context 'do_update: false' do
it 'fills all html fields' do
thing.refresh_markdown_cache!
it 'fills all html fields' do
thing.refresh_markdown_cache
expect(thing.foo_html).to eq(updated_html)
expect(thing.foo_html_changed?).to be_truthy
expect(thing.baz_html_changed?).to be_truthy
end
expect(thing.foo_html).to eq(updated_html)
expect(thing.foo_html_changed?).to be_truthy
expect(thing.baz_html_changed?).to be_truthy
end
it 'does not save the result' do
expect(thing).not_to receive(:update_columns)
it 'does not save the result' do
expect(thing).not_to receive(:update_columns)
thing.refresh_markdown_cache!
end
thing.refresh_markdown_cache
end
it 'updates the markdown cache version' do
thing.cached_markdown_version = nil
thing.refresh_markdown_cache!
it 'updates the markdown cache version' do
thing.cached_markdown_version = nil
thing.refresh_markdown_cache
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
end
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
end
end
context 'do_update: true' do
it 'fills all html fields' do
thing.refresh_markdown_cache!(do_update: true)
describe '#refresh_markdown_cache!' do
before do
thing.foo = updated_markdown
end
expect(thing.foo_html).to eq(updated_html)
expect(thing.foo_html_changed?).to be_truthy
expect(thing.baz_html_changed?).to be_truthy
end
it 'fills all html fields' do
thing.refresh_markdown_cache!
it 'skips saving if not persisted' do
expect(thing).to receive(:persisted?).and_return(false)
expect(thing).not_to receive(:update_columns)
expect(thing.foo_html).to eq(updated_html)
expect(thing.foo_html_changed?).to be_truthy
expect(thing.baz_html_changed?).to be_truthy
end
thing.refresh_markdown_cache!(do_update: true)
end
it 'skips saving if not persisted' do
expect(thing).to receive(:persisted?).and_return(false)
expect(thing).not_to receive(:update_columns)
it 'saves the changes using #update_columns' do
expect(thing).to receive(:persisted?).and_return(true)
expect(thing).to receive(:update_columns)
.with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
thing.refresh_markdown_cache!
end
thing.refresh_markdown_cache!(do_update: true)
end
it 'saves the changes using #update_columns' do
expect(thing).to receive(:persisted?).and_return(true)
expect(thing).to receive(:update_columns)
.with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
thing.refresh_markdown_cache!
end
end
......
......@@ -12,10 +12,10 @@ describe Group, 'Routable' do
it { is_expected.to have_many(:redirect_routes).dependent(:destroy) }
end
describe 'Geo secondary' do
describe 'GitLab read-only instance' do
it 'does not save route if route is not present' do
group.route.path = ''
allow(Gitlab::Geo).to receive(:secondary?).and_return(true)
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
expect(group).to receive(:update_route).and_call_original
expect { group.full_path }.to change { Route.count }.by(0)
......
......@@ -822,8 +822,8 @@ describe Project do
end.to change { project.has_external_issue_tracker}.to(false)
end
it 'does not cache data when in a secondary gitlab geo node' do
allow(Gitlab::Geo).to receive(:secondary?) { true }
it 'does not cache data when in a read-only GitLab instance' do
allow(Gitlab::Database).to receive(:read_only?) { true }
expect do
project.cache_has_external_issue_tracker
......@@ -852,8 +852,8 @@ describe Project do
end.to change { project.has_external_wiki}.to(false)
end
it 'does not cache data when in a secondary gitlab geo node' do
allow(Gitlab::Geo).to receive(:secondary?) { true }
it 'does not cache data when in a read-only GitLab instance' do
allow(Gitlab::Database).to receive(:read_only?) { true }
expect do
project.cache_has_external_wiki
......@@ -2928,7 +2928,7 @@ describe Project do
expect(project.migrate_to_hashed_storage!).to be_truthy
end
it 'flags as readonly' do
it 'flags as read-only' do
expect { project.migrate_to_hashed_storage! }.to change { project.repository_read_only }.to(true)
end
......@@ -3055,7 +3055,7 @@ describe Project do
expect(project.migrate_to_hashed_storage!).to be_nil
end
it 'does not flag as readonly' do
it 'does not flag as read-only' do
expect { project.migrate_to_hashed_storage! }.not_to change { project.repository_read_only }
end
end
......
......@@ -877,8 +877,7 @@ describe 'Git LFS API and storage' do
end
end
describe 'when handling lfs batch request on a secondary Geo node' do
let!(:primary) { create(:geo_node, :primary) }
describe 'when handling lfs batch request on a read-only GitLab instance' do
let(:authorization) { authorize_user }
let(:project) { create(:project) }
let(:path) { "#{project.http_url_to_repo}/info/lfs/objects/batch" }
......@@ -887,7 +886,7 @@ describe 'Git LFS API and storage' do
end
before do
allow(Gitlab::Geo).to receive(:secondary?) { true }
allow(Gitlab::Database).to receive(:read_only?) { true }
project.team << [user, :master]
enable_lfs
end
......@@ -902,7 +901,7 @@ describe 'Git LFS API and storage' do
post_lfs_json path, body.merge('operation' => 'upload'), headers
expect(response).to have_gitlab_http_status(403)
expect(json_response).to include('message' => "You cannot write to a secondary GitLab Geo instance. Please use #{project.http_url_to_repo} instead.")
expect(json_response).to include('message' => 'You cannot write to this read-only GitLab instance.')
end
end
......
......@@ -20,7 +20,7 @@ describe Projects::HashedStorageMigrationService do
expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy
end
it 'updates project to be hashed and not readonly' do
it 'updates project to be hashed and not read-only' do
service.execute
expect(project.hashed_storage?).to be_truthy
......
......@@ -39,9 +39,9 @@ describe Users::ActivityService do
end
end
context 'when in Geo secondary node' do
context 'when in GitLab read-only instance' do
before do
allow(Gitlab::Geo).to receive(:secondary?).and_return(true)
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
end
it 'does not update last_activity_at' do
......
......@@ -12,6 +12,28 @@ describe RepositoryForkWorker do
end
describe "#perform" do
describe 'when a worker was reset without cleanup' do
let(:jid) { '12345678' }
let(:started_project) { create(:project, :repository, :import_started) }
it 'creates a new repository from a fork' do
allow(subject).to receive(:jid).and_return(jid)
expect(shell).to receive(:fork_repository).with(
'/test/path',
project.full_path,
project.repository_storage_path,
fork_project.namespace.full_path
).and_return(true)
subject.perform(
project.id,
'/test/path',
project.full_path,
fork_project.namespace.full_path)
end
end
it "creates a new repository from a fork" do
expect(shell).to receive(:fork_repository).with(
'/test/path',
......
......@@ -6,6 +6,23 @@ describe RepositoryImportWorker do
subject { described_class.new }
describe '#perform' do
context 'when worker was reset without cleanup' do
let(:jid) { '12345678' }
let(:started_project) { create(:project, :import_started, import_jid: jid) }
it 'imports the project successfully' do
allow(subject).to receive(:jid).and_return(jid)
expect_any_instance_of(Projects::ImportService).to receive(:execute)
.and_return({ status: :ok })
expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
expect_any_instance_of(Project).to receive(:import_finish)
subject.perform(project.id)
end
end
context 'when the import was successful' do
it 'imports a project' do
expect_any_instance_of(Projects::ImportService).to receive(:execute)
......
......@@ -2,10 +2,12 @@ require 'rails_helper'
describe RepositoryUpdateMirrorWorker do
describe '#perform' do
let(:jid) { '12345678' }
let!(:project) { create(:project, :mirror, :import_scheduled) }
before do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(true)
allow(subject).to receive(:jid).and_return(jid)
end
it 'sets status as finished when update mirror service executes successfully' do
......@@ -36,16 +38,22 @@ describe RepositoryUpdateMirrorWorker do
expect(project.reload.import_status).to eq('failed')
end
context 'when worker was reset without cleanup' do
let(:started_project) { create(:project, :mirror, :import_started, import_jid: jid) }
it 'sets status as finished when update mirror service executes successfully' do
expect_any_instance_of(Projects::UpdateMirrorService).to receive(:execute).and_return(status: :success)
expect { subject.perform(started_project.id) }.to change { started_project.reload.import_status }.to('finished')
end
end
context 'reschedule mirrors' do
before do
allow_any_instance_of(Projects::UpdateMirrorService).to receive(:execute).and_return(status: :success)
end
context 'when we obtain the lease' do
before do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(true)
end
it 'performs UpdateAllMirrorsWorker when reschedule_immediately? returns true' do
allow(Gitlab::Mirror).to receive(:reschedule_immediately?).and_return(true)
......
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