Commit 479fa52d authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch '202035-geo-http-clone-pull-redirect-to-primary-redirect' into 'master'

Geo: Support HTTP git operations for repos that are not yet replicated

Closes #202035

See merge request gitlab-org/gitlab!27072
parents 4aad18a1 22659d87
...@@ -5,22 +5,33 @@ module EE ...@@ -5,22 +5,33 @@ module EE
module GitHttpClientController module GitHttpClientController
extend ActiveSupport::Concern extend ActiveSupport::Concern
# This module is responsible for determining if an incoming secondary bound # This module is responsible for determining if an incoming Geo secondary
# HTTP request should be redirected to the primary. # bound HTTP request should be redirected to the Primary.
# #
# Why? A secondary is not allowed to perform any write actions, so any # Why? A secondary is not allowed to perform any write actions, so any
# request of this type need to be sent through to the primary. By # request of this type needs to be sent through to the Primary. By
# redirecting within code, we allow clients to git pull/push using their # redirecting within code, we allow clients to git pull/push using their
# secondary git remote without needing an additional primary remote. # secondary git remote without needing an additional primary remote.
# #
# The method for redirection *must* happen as early as possible in the
# request. For example, putting the redirection logic in #access_check
# will not work because the git client will not accept a 302 in response
# to verifying credentials.
#
# Current secondary HTTP requests to redirect: - # Current secondary HTTP requests to redirect: -
# #
# * git pull (repository is not replicated)
# * GET /namespace/repo.git/info/refs?service=git-upload-pack
#
# * git lfs pull (repository is not replicated)
# * GET /namespace/repo.git/gitlab-lfs/objects/<oid>
#
# * git push # * git push
# * GET /repo.git/info/refs?service=git-receive-pack # * GET /namespace/repo.git/info/refs?service=git-receive-pack
# * POST /repo.git/git-receive-pack # * POST /namespace/repo.git/git-receive-pack
# #
# * git lfs push (usually happens automatically as part of a `git push`) # * git lfs push (usually happens automatically as part of a `git push`)
# * POST /repo.git/info/lfs/objects/batch (and we examine # * POST /namespace/repo.git/info/lfs/objects/batch (and we examine
# params[:operation] to ensure we're dealing with an upload request) # params[:operation] to ensure we're dealing with an upload request)
# #
# For more detail, see the following links: # For more detail, see the following links:
...@@ -30,13 +41,13 @@ module EE ...@@ -30,13 +41,13 @@ module EE
# #
prepended do prepended do
prepend_before_action do prepend_before_action do
redirect_to(primary_full_url) if redirect? redirect_to(geo_primary_full_url) if geo_redirect?
end end
end end
private private
class RouteHelper class GeoRouteHelper
attr_reader :controller_name, :action_name attr_reader :controller_name, :action_name
CONTROLLER_AND_ACTIONS_TO_REDIRECT = { CONTROLLER_AND_ACTIONS_TO_REDIRECT = {
...@@ -44,7 +55,8 @@ module EE ...@@ -44,7 +55,8 @@ module EE
'lfs_locks_api' => %w{create unlock verify} 'lfs_locks_api' => %w{create unlock verify}
}.freeze }.freeze
def initialize(controller_name, action_name, service) def initialize(project, controller_name, action_name, service)
@project = project
@controller_name = controller_name @controller_name = controller_name
@action_name = action_name @action_name = action_name
@service = service @service = service
...@@ -56,12 +68,20 @@ module EE ...@@ -56,12 +68,20 @@ module EE
def redirect? def redirect?
!!CONTROLLER_AND_ACTIONS_TO_REDIRECT[controller_name]&.include?(action_name) || !!CONTROLLER_AND_ACTIONS_TO_REDIRECT[controller_name]&.include?(action_name) ||
git_receive_pack_request? git_receive_pack_request? ||
redirect_to_avoid_enumeration? ||
not_yet_replicated_redirect?
end
def not_yet_replicated_redirect?
return false unless project
git_upload_pack_request? && !::Geo::ProjectRegistry.repository_replicated_for?(project.id)
end end
private private
attr_reader :service attr_reader :project, :service
# Examples: # Examples:
# #
...@@ -83,6 +103,14 @@ module EE ...@@ -83,6 +103,14 @@ module EE
service_or_action_name == 'git-receive-pack' service_or_action_name == 'git-receive-pack'
end end
# Matches:
#
# GET /repo.git/info/refs?service=git-upload-pack
#
def git_upload_pack_request?
service_or_action_name == 'git-upload-pack'
end
# Matches: # Matches:
# #
# GET /repo.git/info/refs # GET /repo.git/info/refs
...@@ -90,13 +118,24 @@ module EE ...@@ -90,13 +118,24 @@ module EE
def info_refs_request? def info_refs_request?
action_name == 'info_refs' action_name == 'info_refs'
end end
# The purpose of the #redirect_to_avoid_enumeration? method is to avoid
# a scenario where an authenticated user uses the HTTP responses as a
# way of enumerating private projects. Without this check, an attacker
# could determine if a project exists or not by looking at the initial
# HTTP response code for 401 (doesn't exist) vs 302. (exists).
#
def redirect_to_avoid_enumeration?
project.nil?
end
end end
class GitLFSHelper class GeoGitLFSHelper
MINIMUM_GIT_LFS_VERSION = '2.4.2'.freeze MINIMUM_GIT_LFS_VERSION = '2.4.2'.freeze
def initialize(route_helper, operation, current_version) def initialize(project, geo_route_helper, operation, current_version)
@route_helper = route_helper @project = project
@geo_route_helper = geo_route_helper
@operation = operation @operation = operation
@current_version = current_version @current_version = current_version
end end
...@@ -110,8 +149,8 @@ module EE ...@@ -110,8 +149,8 @@ module EE
end end
def redirect? def redirect?
return false unless route_helper.match?('lfs_api', 'batch') return true if batch_upload?
return true if upload? return true if not_yet_replicated_redirect?
false false
end end
...@@ -124,15 +163,33 @@ module EE ...@@ -124,15 +163,33 @@ module EE
private private
attr_reader :route_helper, :operation, :current_version attr_reader :project, :geo_route_helper, :operation, :current_version
def incorrect_version_message def incorrect_version_message
translation = _("You need git-lfs version %{min_git_lfs_version} (or greater) to continue. Please visit https://git-lfs.github.com") translation = _("You need git-lfs version %{min_git_lfs_version} (or greater) to continue. Please visit https://git-lfs.github.com")
translation % { min_git_lfs_version: MINIMUM_GIT_LFS_VERSION } translation % { min_git_lfs_version: MINIMUM_GIT_LFS_VERSION }
end end
def upload? def batch_request?
operation == 'upload' geo_route_helper.match?('lfs_api', 'batch')
end
def batch_upload?
batch_request? && operation == 'upload'
end
def batch_download?
batch_request? && operation == 'download'
end
def transfer_download?
geo_route_helper.match?('lfs_storage', 'download')
end
def not_yet_replicated_redirect?
return false unless project
(batch_download? || transfer_download?) && !::Geo::ProjectRegistry.repository_replicated_for?(project.id)
end end
def wanted_version def wanted_version
...@@ -140,52 +197,51 @@ module EE ...@@ -140,52 +197,51 @@ module EE
end end
end end
def route_helper def geo_route_helper
@route_helper ||= RouteHelper.new(controller_name, action_name, params[:service]) @geo_route_helper ||= GeoRouteHelper.new(project, controller_name, action_name, params[:service])
end end
def git_lfs_helper def geo_git_lfs_helper
# params[:operation] explained: https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#requests # params[:operation] explained: https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#requests
@git_lfs_helper ||= GitLFSHelper.new(route_helper, params[:operation], request.headers['User-Agent']) @geo_git_lfs_helper ||= GeoGitLFSHelper.new(project, geo_route_helper, params[:operation], request.headers['User-Agent'])
end end
def request_fullpath_for_primary def geo_request_fullpath_for_primary
relative_url_root = ::Gitlab.config.gitlab.relative_url_root.chomp('/') relative_url_root = ::Gitlab.config.gitlab.relative_url_root.chomp('/')
request.fullpath.sub(relative_url_root, '') request.fullpath.sub(relative_url_root, '')
end end
def primary_full_url def geo_primary_full_url
path = File.join(secondary_referrer_path_prefix, request_fullpath_for_primary) path = if geo_route_helper.not_yet_replicated_redirect?
# git clone/pull
geo_request_fullpath_for_primary
else
# git push
File.join(geo_secondary_referrer_path_prefix, geo_request_fullpath_for_primary)
end
::Gitlab::Utils.append_path(::Gitlab::Geo.primary_node.internal_url, path) ::Gitlab::Utils.append_path(::Gitlab::Geo.primary_node.internal_url, path)
end end
def secondary_referrer_path_prefix def geo_secondary_referrer_path_prefix
File.join(::Gitlab::Geo::GitPushHttp::PATH_PREFIX, ::Gitlab::Geo.current_node.id.to_s) File.join(::Gitlab::Geo::GitPushHttp::PATH_PREFIX, ::Gitlab::Geo.current_node.id.to_s)
end end
def redirect? def geo_redirect?
# Don't redirect if we're not a secondary with a primary
return false unless ::Gitlab::Geo.secondary_with_primary? return false unless ::Gitlab::Geo.secondary_with_primary?
return true if geo_route_helper.redirect?
# Redirect as the request matches RouteHelper::CONTROLLER_AND_ACTIONS_TO_REDIRECT if geo_git_lfs_helper.redirect?
return true if route_helper.redirect? return true if geo_git_lfs_helper.version_ok?
# Look to redirect, as we're an LFS batch upload request
if git_lfs_helper.redirect?
# Redirect as git-lfs version is at least 2.4.2
return true if git_lfs_helper.version_ok?
# git-lfs 2.4.2 is really only required for requests that involve # git-lfs 2.4.2 is really only required for requests that involve
# redirection, so we only render if it's an LFS upload operation # redirection, so we only render if it's an LFS upload operation
# #
render(git_lfs_helper.incorrect_version_response) render(geo_git_lfs_helper.incorrect_version_response)
# Don't redirect
return false return false
end end
# Don't redirect
false false
end end
end end
......
...@@ -209,6 +209,12 @@ class Geo::ProjectRegistry < Geo::BaseRegistry ...@@ -209,6 +209,12 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
where(id: start..finish) where(id: start..finish)
end end
def self.repository_replicated_for?(project_id)
return true unless ::Gitlab::Geo.secondary_with_primary?
where(project_id: project_id).where.not(last_repository_successful_sync_at: nil).exists?
end
# Must be run before fetching the repository to avoid a race condition # Must be run before fetching the repository to avoid a race condition
# #
# @param [String] type must be one of the values in TYPES # @param [String] type must be one of the values in TYPES
...@@ -432,6 +438,10 @@ class Geo::ProjectRegistry < Geo::BaseRegistry ...@@ -432,6 +438,10 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
:synced :synced
end end
def repository_has_successfully_synced?
last_repository_successful_sync_at.present?
end
private private
# Whether any operation has ever been attempted # Whether any operation has ever been attempted
......
---
title: Geo - Support git clone/pull operations for repositories that are not yet replicated
merge_request: 27072
author:
type: added
...@@ -8,6 +8,7 @@ module EE ...@@ -8,6 +8,7 @@ module EE
prepended do prepended do
helpers do helpers do
include ::Gitlab::Utils::StrongMemoize
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
override :lfs_authentication_url override :lfs_authentication_url
...@@ -17,23 +18,42 @@ module EE ...@@ -17,23 +18,42 @@ module EE
override :ee_post_receive_response_hook override :ee_post_receive_response_hook
def ee_post_receive_response_hook(response) def ee_post_receive_response_hook(response)
response.add_basic_message(geo_secondary_lag_message) if ::Gitlab::Geo.primary? response.add_basic_message(geo_redirect_to_primary_message) if display_geo_redirect_to_primary_message?
response.add_basic_message(geo_secondary_lag_message) if geo_display_secondary_lag_message?
end end
def geo_secondary_lag_message def geo_display_secondary_lag_message?
lag = current_replication_lag ::Gitlab::Geo.primary? && geo_current_replication_lag.to_i > 0
return if lag.to_i <= 0 end
"Current replication lag: #{lag} seconds" def geo_secondary_lag_message
"Current replication lag: #{geo_current_replication_lag} seconds"
end end
def current_replication_lag def geo_current_replication_lag
fetch_geo_node_referrer&.status&.db_replication_lag_seconds strong_memoize(:geo_current_replication_lag) do
geo_referred_node&.status&.db_replication_lag_seconds
end
end end
def fetch_geo_node_referrer def geo_referred_node
strong_memoize(:geo_referred_node) do
::Gitlab::Geo::GitPushHttp.new(params[:identifier], params[:gl_repository]).fetch_referrer_node ::Gitlab::Geo::GitPushHttp.new(params[:identifier], params[:gl_repository]).fetch_referrer_node
end end
end
def display_geo_redirect_to_primary_message?
::Gitlab::Geo.primary? && geo_redirect_to_primary_message
end
def geo_redirect_to_primary_message
return unless geo_referred_node
@geo_redirect_to_primary_message ||= begin
url = "#{::Gitlab::Geo.current_node.url.chomp('/')}/#{project.full_path}.git"
::Gitlab::Geo.redirecting_push_to_primary_message(url)
end
end
override :check_allowed override :check_allowed
def check_allowed(params) def check_allowed(params)
......
...@@ -40,7 +40,7 @@ module EE ...@@ -40,7 +40,7 @@ module EE
end end
def messages def messages
messages = proxying_to_primary_message messages = ::Gitlab::Geo.proxying_push_to_primary_message(primary_ssh_url_to_repo).split("\n")
lag_message = current_replication_lag_message lag_message = current_replication_lag_message
return messages unless lag_message return messages unless lag_message
...@@ -75,23 +75,6 @@ module EE ...@@ -75,23 +75,6 @@ module EE
geo_primary_ssh_url_to_repo(project_or_wiki) geo_primary_ssh_url_to_repo(project_or_wiki)
end end
def proxying_to_primary_message
# This is formatted like this to fit into the console 'box', e.g.
#
# remote:
# remote: You're pushing to a Geo secondary! We'll help you by proxying this
# remote: request to the primary:
# remote:
# remote: ssh://<user>@<host>:<port>/<group>/<repo>.git
# remote:
<<~STR.split("\n")
You're pushing to a Geo secondary! We'll help you by proxying this
request to the primary:
#{primary_ssh_url_to_repo}
STR
end
def current_replication_lag_message def current_replication_lag_message
return if ::Gitlab::Database.read_write? || current_replication_lag.zero? return if ::Gitlab::Database.read_write? || current_replication_lag.zero?
......
...@@ -136,5 +136,34 @@ module Gitlab ...@@ -136,5 +136,34 @@ module Gitlab
Gitlab::CIDR.new(allowed_ips).match?(ip) Gitlab::CIDR.new(allowed_ips).match?(ip)
end end
def self.proxying_push_to_primary_message(url)
push_to_primary_message(url, 'proxying')
end
def self.redirecting_push_to_primary_message(url)
push_to_primary_message(url, 'redirecting')
end
def self.push_to_primary_message(url, action)
return unless url && action
# This is formatted like this to fit into the console 'box', e.g.
#
# remote:
# remote: You're pushing to a Geo secondary! We'll help you by <action> this
# remote: request to the primary:
# remote:
# remote: <url>
# remote:
template = <<~STR
You're pushing to a Geo secondary! We'll help you by %{action} this
request to the primary:
%{url}
STR
_(template) % { action: _(action), url: url }
end
end end
end end
...@@ -307,4 +307,32 @@ describe Gitlab::Geo, :geo, :request_store do ...@@ -307,4 +307,32 @@ describe Gitlab::Geo, :geo, :request_store do
end end
end end
end end
describe '.proxying_to_primary_message' do
it 'returns a message as a string' do
url = 'ssh://git@primary.com/namespace/repo.git'
message = <<~STR
You're pushing to a Geo secondary! We'll help you by proxying this
request to the primary:
#{url}
STR
expect(described_class.proxying_push_to_primary_message(url)).to eq(message)
end
end
describe '.redirecting_to_primary_message' do
it 'returns a message as a string' do
url = 'http://primary.com/namespace/repo.git'
message = <<~STR
You're pushing to a Geo secondary! We'll help you by redirecting this
request to the primary:
#{url}
STR
expect(described_class.redirecting_push_to_primary_message(url)).to eq(message)
end
end
end end
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe Geo::ProjectRegistry do describe Geo::ProjectRegistry do
include ::EE::GeoHelpers
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
set(:project) { create(:project, description: 'kitten mittens') } set(:project) { create(:project, description: 'kitten mittens') }
...@@ -210,6 +211,72 @@ describe Geo::ProjectRegistry do ...@@ -210,6 +211,72 @@ describe Geo::ProjectRegistry do
end end
end end
describe '.repository_replicated_for?' do
context 'for a non-Geo setup' do
it 'returns true' do
expect(described_class.repository_replicated_for?(project.id)).to be_truthy
end
end
context 'for a Geo setup' do
before do
stub_current_geo_node(current_node)
end
context 'for a Geo Primary' do
let(:current_node) { create(:geo_node, :primary) }
it 'returns true' do
expect(described_class.repository_replicated_for?(project.id)).to be_truthy
end
end
context 'for a Geo secondary' do
let(:current_node) { create(:geo_node) }
context 'where Primary node is not configured' do
it 'returns true' do
expect(described_class.repository_replicated_for?(project.id)).to be_truthy
end
end
context 'where Primary node is configured' do
before do
create(:geo_node, :primary)
end
context 'where project_registry entry does not exist' do
it 'returns false' do
project_without_registry = create(:project)
expect(described_class.repository_replicated_for?(project_without_registry.id)).to be_falsey
end
end
context 'where project_registry entry does exist' do
context 'where last_repository_successful_sync_at is not set' do
it 'returns false' do
project_with_failed_registry = create(:project)
create(:geo_project_registry, :repository_sync_failed, project: project_with_failed_registry)
expect(described_class.repository_replicated_for?(project_with_failed_registry.id)).to be_falsey
end
end
context 'where last_repository_successful_sync_at is set' do
it 'returns true' do
project_with_synced_registry = create(:project)
create(:geo_project_registry, :synced, project: project_with_synced_registry)
expect(described_class.repository_replicated_for?(project_with_synced_registry.id)).to be_truthy
end
end
end
end
end
end
end
describe '#repository_sync_due?' do describe '#repository_sync_due?' do
where(:last_synced_at, :resync, :retry_at, :expected) do where(:last_synced_at, :resync, :retry_at, :expected) do
now = Time.now now = Time.now
...@@ -1014,4 +1081,22 @@ describe Geo::ProjectRegistry do ...@@ -1014,4 +1081,22 @@ describe Geo::ProjectRegistry do
expect(registry.synchronization_state).to eq(:synced) expect(registry.synchronization_state).to eq(:synced)
end end
end end
describe 'repository_has_successfully_synced?' do
context 'when repository has never successfully synced' do
it 'returns false' do
registry = create(:geo_project_registry, last_repository_successful_sync_at: nil)
expect(registry.repository_has_successfully_synced?).to be_falsey
end
end
context 'when repository has successfully synced' do
it 'returns true' do
registry = create(:geo_project_registry, last_repository_successful_sync_at: Time.now)
expect(registry.repository_has_successfully_synced?).to be_truthy
end
end
end
end end
...@@ -4,8 +4,10 @@ require 'spec_helper' ...@@ -4,8 +4,10 @@ require 'spec_helper'
describe API::Internal::Base do describe API::Internal::Base do
include EE::GeoHelpers include EE::GeoHelpers
let_it_be(:primary_node, reload: true) { create(:geo_node, :primary) } let_it_be(:primary_url) { 'http://primary.example.com' }
let_it_be(:secondary_node, reload: true) { create(:geo_node) } let_it_be(:secondary_url) { 'http://secondary.example.com' }
let_it_be(:primary_node, reload: true) { create(:geo_node, :primary, url: primary_url) }
let_it_be(:secondary_node, reload: true) { create(:geo_node, url: secondary_url) }
describe 'POST /internal/post_receive', :geo do describe 'POST /internal/post_receive', :geo do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
...@@ -96,6 +98,23 @@ describe API::Internal::Base do ...@@ -96,6 +98,23 @@ describe API::Internal::Base do
expect(json_response['messages']).not_to include({ 'message' => a_string_matching('replication lag'), 'type' => anything }) expect(json_response['messages']).not_to include({ 'message' => a_string_matching('replication lag'), 'type' => anything })
end end
end end
it 'includes a message advising a redirection occurred' do
redirect_message = <<~STR
You're pushing to a Geo secondary! We'll help you by redirecting this
request to the primary:
http://primary.example.com/#{project.full_path}.git
STR
post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['messages']).to include({
'type' => 'basic',
'message' => redirect_message
})
end
end end
context 'when the push was not redirected from a Geo secondary to the primary' do context 'when the push was not redirected from a Geo secondary to the primary' do
......
...@@ -9,9 +9,13 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -9,9 +9,13 @@ describe "Git HTTP requests (Geo)", :geo do
include WorkhorseHelpers include WorkhorseHelpers
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project, :repository, :private) } let_it_be(:project_with_repo) { create(:project, :repository, :private) }
let_it_be(:primary) { create(:geo_node, :primary) } let_it_be(:project_no_repo) { create(:project, :private) }
let_it_be(:secondary) { create(:geo_node) }
let_it_be(:primary_url) { 'http://primary.example.com' }
let_it_be(:secondary_url) { 'http://secondary.example.com' }
let_it_be(:primary) { create(:geo_node, :primary, url: primary_url) }
let_it_be(:secondary) { create(:geo_node, url: secondary_url) }
# Ensure the token always comes from the real time of the request # Ensure the token always comes from the real time of the request
let(:auth_token) { Gitlab::Geo::BaseRequest.new(scope: project.full_path).authorization } let(:auth_token) { Gitlab::Geo::BaseRequest.new(scope: project.full_path).authorization }
...@@ -26,8 +30,12 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -26,8 +30,12 @@ describe "Git HTTP requests (Geo)", :geo do
let(:auth_token_with_invalid_scope) { Gitlab::Geo::BaseRequest.new(scope: "invalid").authorization } let(:auth_token_with_invalid_scope) { Gitlab::Geo::BaseRequest.new(scope: "invalid").authorization }
before do before do
project.add_maintainer(user) project_with_repo.add_maintainer(user)
project.add_guest(user_without_push_access) project_with_repo.add_guest(user_without_push_access)
create(:geo_project_registry, :synced, project: project_with_repo)
project_no_repo.add_maintainer(user)
project_no_repo.add_guest(user_without_push_access)
stub_licensed_features(geo: true) stub_licensed_features(geo: true)
stub_current_geo_node(current_node) stub_current_geo_node(current_node)
...@@ -36,7 +44,56 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -36,7 +44,56 @@ describe "Git HTTP requests (Geo)", :geo do
auth_token auth_token
end end
shared_examples_for 'Geo request' do shared_examples_for 'a Geo 200 git request' do
subject do
make_request
response
end
context 'valid Geo JWT token' do
it 'returns an OK response with JSON data' do
is_expected.to have_gitlab_http_status(:ok)
expect(response.content_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response).to include('ShowAllRefs' => true)
end
end
end
shared_examples_for 'a Geo 200 git-lfs request' do
subject do
make_request
response
end
context 'valid Geo JWT token' do
it 'returns an OK response with binary data' do
is_expected.to have_gitlab_http_status(:ok)
expect(response.content_type).to eq('application/octet-stream')
end
end
end
shared_examples_for 'a Geo 302 redirect to Primary' do
# redirect_url needs to be defined when using this shared example
subject do
make_request
response
end
context 'valid Geo JWT token' do
it 'returns a redirect response' do
is_expected.to have_gitlab_http_status(:redirect)
expect(response.content_type).to eq('text/html')
expect(response.headers['Location']).to eq(redirect_url)
end
end
end
shared_examples_for 'a Geo git request' do
subject do subject do
make_request make_request
response response
...@@ -56,15 +113,6 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -56,15 +113,6 @@ describe "Git HTTP requests (Geo)", :geo do
it { is_expected.to have_gitlab_http_status(:unauthorized) } it { is_expected.to have_gitlab_http_status(:unauthorized) }
end end
context 'valid Geo JWT token' do
it 'returns an OK response' do
is_expected.to have_gitlab_http_status(:ok)
expect(response.content_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response).to include('ShowAllRefs' => true)
end
end
context 'no Geo JWT token' do context 'no Geo JWT token' do
let(:env) { workhorse_internal_api_request_header } let(:env) { workhorse_internal_api_request_header }
...@@ -83,42 +131,106 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -83,42 +131,106 @@ describe "Git HTTP requests (Geo)", :geo do
context 'when current node is a secondary' do context 'when current node is a secondary' do
let(:current_node) { secondary } let(:current_node) { secondary }
let_it_be(:project) { create(:project, :repository, :private) }
describe 'GET info_refs' do describe 'GET info_refs' do
context 'git pull' do context 'git pull' do
def make_request def make_request
get "/#{project.full_path}.git/info/refs", params: { service: 'git-upload-pack' }, headers: env get "/#{project.full_path}.git/info/refs", params: { service: 'git-upload-pack' }, headers: env
end end
it_behaves_like 'Geo request' context 'when the repository exists' do
context 'but has not successfully synced' do
let_it_be(:project_with_repo_but_not_synced) { create(:project, :repository, :private) }
let_it_be(:project) { project_with_repo_but_not_synced }
let(:redirect_url) { "#{primary_url}/#{project_with_repo_but_not_synced.full_path}.git/info/refs?service=git-upload-pack" }
before do
create(:geo_project_registry, :synced, project: project_with_repo_but_not_synced, last_repository_successful_sync_at: nil)
end
it_behaves_like 'a Geo 302 redirect to Primary'
context 'when terms are enforced' do
before do
enforce_terms
end
it_behaves_like 'a Geo 302 redirect to Primary'
end
end
context 'and has successfully synced' do
let_it_be(:project) { project_with_repo }
it_behaves_like 'a Geo git request'
it_behaves_like 'a Geo 200 git request'
context 'when terms are enforced' do context 'when terms are enforced' do
before do before do
enforce_terms enforce_terms
end end
it_behaves_like 'Geo request' it_behaves_like 'a Geo git request'
it_behaves_like 'a Geo 200 git request'
end
end
end
context 'when the repository does not exist' do
let_it_be(:project) { project_no_repo }
let(:redirect_url) { "#{primary_url}/#{project.full_path}.git/info/refs?service=git-upload-pack" }
it_behaves_like 'a Geo 302 redirect to Primary'
context 'when terms are enforced' do
before do
enforce_terms
end
it_behaves_like 'a Geo 302 redirect to Primary'
end
end
context 'when the project does not exist' do
# Override #make_request so we can use a non-existent project path
def make_request
get "/#{non_existent_project_path}.git/info/refs", params: { service: 'git-upload-pack' }, headers: env
end
let(:project) { nil }
let(:auth_token) { nil }
let(:non_existent_project_path) { 'non-existent-namespace/non-existent-project' }
let(:endpoint_path) { "/#{non_existent_project_path}.git/info/refs?service=git-upload-pack" }
let(:redirect_url) { redirected_primary_url }
# To avoid enumeration of private projects not in selective sync,
# this response must be the same as a private project without a repo.
it_behaves_like 'a Geo 302 redirect to Primary'
context 'when terms are enforced' do
before do
enforce_terms
end
it_behaves_like 'a Geo 302 redirect to Primary'
end
end end
end end
context 'git push' do context 'git push' do
def make_request def make_request
get url, params: { service: 'git-receive-pack' }, headers: env get endpoint_path, params: { service: 'git-receive-pack' }, headers: env
end end
let(:url) { "/#{project.full_path}.git/info/refs" } let_it_be(:project) { project_with_repo }
let(:endpoint_path) { "/#{project.full_path}.git/info/refs" }
let(:redirect_url) { "#{redirected_primary_url}?service=git-receive-pack" }
subject do subject do
make_request make_request
response response
end end
it 'redirects to the primary' do it_behaves_like 'a Geo 302 redirect to Primary'
is_expected.to have_gitlab_http_status(:redirect)
redirect_location = "#{redirected_primary_url}?service=git-receive-pack"
expect(subject.header['Location']).to eq(redirect_location)
end
end end
end end
...@@ -127,26 +239,48 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -127,26 +239,48 @@ describe "Git HTTP requests (Geo)", :geo do
post "/#{project.full_path}.git/git-upload-pack", params: {}, headers: env post "/#{project.full_path}.git/git-upload-pack", params: {}, headers: env
end end
it_behaves_like 'Geo request' context 'when the repository exists' do
let_it_be(:project) { project_with_repo }
it_behaves_like 'a Geo git request'
it_behaves_like 'a Geo 200 git request'
context 'when terms are enforced' do
before do
enforce_terms
end
it_behaves_like 'a Geo git request'
it_behaves_like 'a Geo 200 git request'
end
end
context 'when the repository does not exist' do
let_it_be(:project) { project_no_repo }
let(:redirect_url) { "#{primary_url}/#{project.full_path}.git/git-upload-pack" }
it_behaves_like 'a Geo 302 redirect to Primary'
context 'when terms are enforced' do context 'when terms are enforced' do
before do before do
enforce_terms enforce_terms
end end
it_behaves_like 'Geo request' it_behaves_like 'a Geo 302 redirect to Primary'
end
end end
end end
context 'git-lfs' do context 'git-lfs' do
context 'API' do context 'Batch API' do
describe 'POST batch' do describe 'POST /namespace/repo.git/info/lfs/objects/batch' do
def make_request def make_request
post url, params: args, headers: env post endpoint_path, params: args, headers: env
end end
let(:args) { {} } let(:args) { {} }
let(:url) { "/#{project.full_path}.git/info/lfs/objects/batch" } let_it_be(:project) { project_with_repo }
let(:endpoint_path) { "/#{project.full_path}.git/info/lfs/objects/batch" }
subject do subject do
make_request make_request
...@@ -161,20 +295,17 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -161,20 +295,17 @@ describe "Git HTTP requests (Geo)", :geo do
context 'operation upload' do context 'operation upload' do
let(:args) { { 'operation' => 'upload' }.to_json } let(:args) { { 'operation' => 'upload' }.to_json }
let(:redirect_url) { redirected_primary_url }
context 'with the correct git-lfs version' do context 'with a valid git-lfs version' do
before do before do
env['User-Agent'] = 'git-lfs/2.4.2 (GitHub; darwin amd64; go 1.10.2)' env['User-Agent'] = 'git-lfs/2.4.2 (GitHub; darwin amd64; go 1.10.2)'
end end
it 'redirects to the primary' do it_behaves_like 'a Geo 302 redirect to Primary'
is_expected.to have_gitlab_http_status(:redirect)
redirect_location = "#{redirected_primary_url}"
expect(subject.header['Location']).to eq(redirect_location)
end
end end
context 'with an incorrect git-lfs version' do context 'with an invalid git-lfs version' do
where(:description, :version) do where(:description, :version) do
'outdated' | 'git-lfs/2.4.1' 'outdated' | 'git-lfs/2.4.1'
'unknown' | 'git-lfs' 'unknown' | 'git-lfs'
...@@ -209,11 +340,23 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -209,11 +340,23 @@ describe "Git HTTP requests (Geo)", :geo do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
env['Authorization'] = authorization env['Authorization'] = authorization
env['User-Agent'] = "2.4.2 (GitHub; darwin amd64; go 1.10.2)"
end end
context 'when the repository exists' do
let_it_be(:project) { project_with_repo }
it 'is handled by the secondary' do it 'is handled by the secondary' do
is_expected.to have_gitlab_http_status(:ok) is_expected.to have_gitlab_http_status(:ok)
end end
end
context 'when the repository does not exist' do
let_it_be(:project) { project_no_repo }
let(:redirect_url) { redirected_primary_url }
it_behaves_like 'a Geo 302 redirect to Primary'
end
where(:description, :version) do where(:description, :version) do
'outdated' | 'git-lfs/2.4.1' 'outdated' | 'git-lfs/2.4.1'
...@@ -235,6 +378,47 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -235,6 +378,47 @@ describe "Git HTTP requests (Geo)", :geo do
end end
end end
context 'Transfer API' do
describe 'GET /namespace/repo.git/gitlab-lfs/objects/<oid>' do
def make_request
get endpoint_path, headers: env
end
let(:user) { create(:user) }
let(:lfs_object) { create(:lfs_object, :with_file) }
let(:endpoint_path) { "/#{project.full_path}.git/gitlab-lfs/objects/#{lfs_object.oid}" }
let(:authorization) { ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) }
subject do
make_request
response
end
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
project.update_attribute(:lfs_enabled, true)
project.lfs_objects << lfs_object
project.add_maintainer(user)
env['Authorization'] = authorization
env['User-Agent'] = "2.4.2 (GitHub; darwin amd64; go 1.10.2)"
end
context 'when the repository exists' do
let_it_be(:project) { project_with_repo }
it_behaves_like 'a Geo 200 git-lfs request'
end
context 'when the repository does not exist' do
let_it_be(:project) { project_no_repo }
let(:redirect_url) { redirected_primary_url }
it_behaves_like 'a Geo 302 redirect to Primary'
end
end
end
context 'Locks API' do context 'Locks API' do
where(:description, :path, :args) do where(:description, :path, :args) do
'create' | 'info/lfs/locks' | {} 'create' | 'info/lfs/locks' | {}
...@@ -245,28 +429,26 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -245,28 +429,26 @@ describe "Git HTTP requests (Geo)", :geo do
with_them do with_them do
describe "POST #{description}" do describe "POST #{description}" do
def make_request def make_request
post url, params: args, headers: env post endpoint_path, params: args, headers: env
end end
let(:url) { "/#{project.full_path}.git/#{path}" } let_it_be(:project) { project_with_repo }
let(:endpoint_path) { "/#{project.full_path}.git/#{path}" }
let(:redirect_url) { redirected_primary_url }
subject do subject do
make_request make_request
response response
end end
it 'redirects to the primary' do it_behaves_like 'a Geo 302 redirect to Primary'
is_expected.to have_gitlab_http_status(:redirect)
redirect_location = "#{redirected_primary_url}"
expect(subject.header['Location']).to eq(redirect_location)
end
end end
end end
end end
end end
def redirected_primary_url def redirected_primary_url
"#{primary.url.chomp('/')}#{::Gitlab::Geo::GitPushHttp::PATH_PREFIX}/#{secondary.id}#{url}" "#{primary.url.chomp('/')}#{::Gitlab::Geo::GitPushHttp::PATH_PREFIX}/#{secondary.id}#{endpoint_path}"
end end
end end
...@@ -281,12 +463,13 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -281,12 +463,13 @@ describe "Git HTTP requests (Geo)", :geo do
context 'when HTTP redirected from a secondary node' do context 'when HTTP redirected from a secondary node' do
def make_request def make_request
post url, headers: auth_env(user.username, user.password, nil) post endpoint_path, headers: auth_env(user.username, user.password, nil)
end end
let(:identifier) { "user-#{user.id}" } let(:identifier) { "user-#{user.id}" }
let_it_be(:project) { project_with_repo }
let(:gl_repository) { "project-#{project.id}" } let(:gl_repository) { "project-#{project.id}" }
let(:url) { "#{::Gitlab::Geo::GitPushHttp::PATH_PREFIX}/#{secondary.id}/#{project.full_path}.git/git-receive-pack" } let(:endpoint_path) { "#{::Gitlab::Geo::GitPushHttp::PATH_PREFIX}/#{secondary.id}/#{project.full_path}.git/git-receive-pack" }
# The bigger picture request flow relevant to this feature is: # The bigger picture request flow relevant to this feature is:
# #
...@@ -314,10 +497,11 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -314,10 +497,11 @@ describe "Git HTTP requests (Geo)", :geo do
context 'when proxying an SSH request from a secondary node' do context 'when proxying an SSH request from a secondary node' do
def make_request def make_request
post url, params: {}, headers: env post endpoint_path, params: {}, headers: env
end end
let(:url) { "/#{project.full_path}.git/git-receive-pack" } let_it_be(:project) { project_with_repo }
let(:endpoint_path) { "/#{project.full_path}.git/git-receive-pack" }
before do before do
env['Geo-GL-Id'] = geo_gl_id env['Geo-GL-Id'] = geo_gl_id
...@@ -401,11 +585,12 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -401,11 +585,12 @@ describe "Git HTTP requests (Geo)", :geo do
def make_request def make_request
full_path = project.full_path full_path = project.full_path
project.destroy
get "/#{full_path}.git/info/refs", params: { service: 'git-upload-pack' }, headers: env get "/#{full_path}.git/info/refs", params: { service: 'git-upload-pack' }, headers: env
end end
let_it_be(:project) { project_no_repo }
it { is_expected.to have_gitlab_http_status(:not_found) } it { is_expected.to have_gitlab_http_status(:not_found) }
end end
...@@ -419,6 +604,8 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -419,6 +604,8 @@ describe "Git HTTP requests (Geo)", :geo do
get "/#{repository_path}.git/info/refs", params: { service: 'git-upload-pack' }, headers: env get "/#{repository_path}.git/info/refs", params: { service: 'git-upload-pack' }, headers: env
end end
let_it_be(:project) { project_with_repo }
shared_examples_for 'unauthorized because of invalid scope' do shared_examples_for 'unauthorized because of invalid scope' do
it { is_expected.to have_gitlab_http_status(:unauthorized) } it { is_expected.to have_gitlab_http_status(:unauthorized) }
...@@ -466,6 +653,7 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -466,6 +653,7 @@ describe "Git HTTP requests (Geo)", :geo do
get "/#{repository_path}.git/info/refs", params: { service: 'git-upload-pack' }, headers: env get "/#{repository_path}.git/info/refs", params: { service: 'git-upload-pack' }, headers: env
end end
let_it_be(:project) { project_with_repo }
let(:repository_path) { project.full_path } let(:repository_path) { project.full_path }
it 'returns unauthorized error' do it 'returns unauthorized error' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment