Commit 84031c99 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch...

Merge branch '202037-geo-ssh-clone-pull-redirect-to-primary-when-selective-sync-enabled-and-project-not-selected' into 'master'

Geo: Proxy SSH git operations for repositories that are not yet replicated

Closes #202037

See merge request gitlab-org/gitlab!27994
parents 976c4e52 8da274de
---
title: 'Geo: Proxy SSH git operations for repositories that are not yet replicated'
merge_request: 27994
author:
type: added
...@@ -87,6 +87,51 @@ module API ...@@ -87,6 +87,51 @@ module API
resource 'proxy_git_ssh' do resource 'proxy_git_ssh' do
format :json format :json
# For git clone/pull
# Responsible for making HTTP GET /repo.git/info/refs?service=git-upload-pack
# request *from* secondary gitlab-shell to primary
#
params do
requires :secret_token, type: String
requires :data, type: Hash do
requires :gl_id, type: String
requires :primary_repo, type: String
end
end
post 'info_refs_upload_pack' do
authenticate_by_gitlab_shell_token!
params.delete(:secret_token)
response = Gitlab::Geo::GitSSHProxy.new(params['data']).info_refs_upload_pack
status(response.code)
response.body
end
# Responsible for making HTTP POST /repo.git/git-upload-pack
# request *from* secondary gitlab-shell to primary
#
params do
requires :secret_token, type: String
requires :data, type: Hash do
requires :gl_id, type: String
requires :primary_repo, type: String
end
requires :output, type: String, desc: 'Output from git-upload-pack'
end
post 'upload_pack' do
authenticate_by_gitlab_shell_token!
params.delete(:secret_token)
response = Gitlab::Geo::GitSSHProxy.new(params['data']).upload_pack(params['output'])
status(response.code)
response.body
end
# For git push
# Responsible for making HTTP GET /repo.git/info/refs?service=git-receive-pack # Responsible for making HTTP GET /repo.git/info/refs?service=git-receive-pack
# request *from* secondary gitlab-shell to primary # request *from* secondary gitlab-shell to primary
# #
......
...@@ -12,20 +12,13 @@ module EE ...@@ -12,20 +12,13 @@ module EE
private private
def custom_action_for?(cmd)
return unless receive_pack?(cmd) # git push
return unless ::Gitlab::Database.read_only?
::Gitlab::Geo.secondary_with_primary?
end
def custom_action_for(cmd) def custom_action_for(cmd)
return unless custom_action_for?(cmd) return unless custom_action_for?(cmd)
payload = { payload = {
'action' => 'geo_proxy_to_primary', 'action' => 'geo_proxy_to_primary',
'data' => { 'data' => {
'api_endpoints' => custom_action_api_endpoints, 'api_endpoints' => custom_action_api_endpoints_for(cmd),
'primary_repo' => primary_http_repo_url 'primary_repo' => primary_http_repo_url
} }
} }
...@@ -33,6 +26,17 @@ module EE ...@@ -33,6 +26,17 @@ module EE
::Gitlab::GitAccessResult::CustomAction.new(payload, messages) ::Gitlab::GitAccessResult::CustomAction.new(payload, messages)
end end
def custom_action_for?(cmd)
return unless ::Gitlab::Database.read_only?
return unless ::Gitlab::Geo.secondary_with_primary?
receive_pack?(cmd) || upload_pack_and_not_replicated?(cmd)
end
def upload_pack_and_not_replicated?(cmd)
upload_pack?(cmd) && !::Geo::ProjectRegistry.repository_replicated_for?(project.id)
end
def messages def messages
messages = ::Gitlab::Geo.interacting_with_primary_message(primary_ssh_url_to_repo).split("\n") messages = ::Gitlab::Geo.interacting_with_primary_message(primary_ssh_url_to_repo).split("\n")
lag_message = current_replication_lag_message lag_message = current_replication_lag_message
...@@ -79,7 +83,18 @@ module EE ...@@ -79,7 +83,18 @@ module EE
@current_replication_lag ||= ::Gitlab::Geo::HealthCheck.new.db_replication_lag_seconds @current_replication_lag ||= ::Gitlab::Geo::HealthCheck.new.db_replication_lag_seconds
end end
def custom_action_api_endpoints def custom_action_api_endpoints_for(cmd)
receive_pack?(cmd) ? custom_action_push_api_endpoints : custom_action_pull_api_endpoints
end
def custom_action_pull_api_endpoints
[
api_v4_geo_proxy_git_ssh_info_refs_upload_pack_path,
api_v4_geo_proxy_git_ssh_upload_pack_path
]
end
def custom_action_push_api_endpoints
[ [
api_v4_geo_proxy_git_ssh_info_refs_receive_pack_path, api_v4_geo_proxy_git_ssh_info_refs_receive_pack_path,
api_v4_geo_proxy_git_ssh_receive_pack_path api_v4_geo_proxy_git_ssh_receive_pack_path
......
...@@ -38,14 +38,9 @@ module EE ...@@ -38,14 +38,9 @@ module EE
end end
def geo_proxy_git_ssh_route? def geo_proxy_git_ssh_route?
routes = ::Gitlab::Middleware::ReadOnly::API_VERSIONS.map do |version| ::Gitlab::Middleware::ReadOnly::API_VERSIONS.any? do |version|
%W( request.path.start_with?("/api/v#{version}/geo/proxy_git_ssh")
/api/v#{version}/geo/proxy_git_ssh/info_refs_receive_pack
/api/v#{version}/geo/proxy_git_ssh/receive_pack
)
end end
routes.flatten.include?(request.path)
end end
def geo_api_route? def geo_api_route?
......
...@@ -5,6 +5,9 @@ module Gitlab ...@@ -5,6 +5,9 @@ module Gitlab
class GitSSHProxy class GitSSHProxy
HTTP_READ_TIMEOUT = 60 HTTP_READ_TIMEOUT = 60
UPLOAD_PACK_REQUEST_CONTENT_TYPE = 'application/x-git-upload-pack-request'.freeze
UPLOAD_PACK_RESULT_CONTENT_TYPE = 'application/x-git-upload-pack-result'.freeze
RECEIVE_PACK_REQUEST_CONTENT_TYPE = 'application/x-git-receive-pack-request'.freeze RECEIVE_PACK_REQUEST_CONTENT_TYPE = 'application/x-git-receive-pack-request'.freeze
RECEIVE_PACK_RESULT_CONTENT_TYPE = 'application/x-git-receive-pack-result'.freeze RECEIVE_PACK_RESULT_CONTENT_TYPE = 'application/x-git-receive-pack-result'.freeze
...@@ -49,7 +52,38 @@ module Gitlab ...@@ -49,7 +52,38 @@ module Gitlab
@data = data @data = data
end end
# For git clone/pull
def info_refs_upload_pack
ensure_secondary!
url = "#{primary_repo}/info/refs?service=git-upload-pack"
resp = get(url)
resp.body = remove_upload_pack_http_service_fragment_from(resp.body) if resp.is_a?(Net::HTTPSuccess)
APIResponse.from_http_response(resp, primary_repo)
rescue => e
handle_exception(e)
end
def upload_pack(encoded_response)
ensure_secondary!
url = "#{primary_repo}/git-upload-pack"
headers = { 'Content-Type' => UPLOAD_PACK_REQUEST_CONTENT_TYPE, 'Accept' => UPLOAD_PACK_RESULT_CONTENT_TYPE }
decoded_response = Base64.decode64(encoded_response)
decoded_response = convert_upload_pack_from_http_to_ssh(decoded_response)
resp = post(url, decoded_response, headers)
APIResponse.from_http_response(resp, primary_repo)
rescue => e
handle_exception(e)
end
# For git push # For git push
def info_refs_receive_pack def info_refs_receive_pack
ensure_secondary! ensure_secondary!
...@@ -63,14 +97,14 @@ module Gitlab ...@@ -63,14 +97,14 @@ module Gitlab
handle_exception(e) handle_exception(e)
end end
# For git push def receive_pack(encoded_response)
def receive_pack(encoded_info_refs_response)
ensure_secondary! ensure_secondary!
url = "#{primary_repo}/git-receive-pack" url = "#{primary_repo}/git-receive-pack"
headers = { 'Content-Type' => RECEIVE_PACK_REQUEST_CONTENT_TYPE, 'Accept' => RECEIVE_PACK_RESULT_CONTENT_TYPE } headers = { 'Content-Type' => RECEIVE_PACK_REQUEST_CONTENT_TYPE, 'Accept' => RECEIVE_PACK_RESULT_CONTENT_TYPE }
info_refs_response = Base64.decode64(encoded_info_refs_response) decoded_response = Base64.decode64(encoded_response)
resp = post(url, info_refs_response, headers)
resp = post(url, decoded_response, headers)
APIResponse.from_http_response(resp, primary_repo) APIResponse.from_http_response(resp, primary_repo)
rescue => e rescue => e
...@@ -130,16 +164,38 @@ module Gitlab ...@@ -130,16 +164,38 @@ module Gitlab
http.start { http.request(req) } http.start { http.request(req) }
end end
def remove_receive_pack_http_service_fragment_from(body)
# HTTP(S) and SSH responses are very similar, except for the fragment below. # HTTP(S) and SSH responses are very similar, except for the fragment below.
# As we're performing a git HTTP(S) request here, we'll get a HTTP(s) # As we're performing a git HTTP(S) request here, we'll get a HTTP(s)
# suitable git response. However, we're executing in the context of an # suitable git response. However, we're executing in the context of an
# SSH session so we need to make the response suitable for what git over # SSH session so we need to make the response suitable for what git over
# SSH expects. # SSH expects.
# See Uploading Data > HTTP(S) section at:
# https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
#
def remove_upload_pack_http_service_fragment_from(body)
body.gsub(/\A001e# service=git-upload-pack\n0000/, '')
end
# See Uploading Data > HTTP(S) section at:
# https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
# #
def convert_upload_pack_from_http_to_ssh(body)
clone_operation = /\n0032want \w{40}/
if body.match?(clone_operation)
# git clone
body.gsub(clone_operation, '') + "\n0000"
else
# git pull
body.gsub(/\n0000$/, "\n0009done\n0000")
end
end
# See Downloading Data > HTTP(S) section at: # See Downloading Data > HTTP(S) section at:
# https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols # https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
# #
def remove_receive_pack_http_service_fragment_from(body)
body.gsub(/\A001f# service=git-receive-pack\n0000/, '') body.gsub(/\A001f# service=git-receive-pack\n0000/, '')
end end
......
...@@ -17,7 +17,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -17,7 +17,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do
let(:base_request) { double(Gitlab::Geo::BaseRequest.new.authorization) } let(:base_request) { double(Gitlab::Geo::BaseRequest.new.authorization) }
let(:info_refs_body_short) do let(:info_refs_body_short) do
"008f43ba78b7912f7bf7ef1d7c3b8a0e5ae14a759dfa refs/heads/masterreport-status delete-refs side-band-64k quiet atomic ofs-delta agent=git/2.18.0\n0000" "008f43ba78b7912f7bf7ef1d7c3b8a0e5ae14a759dfa refs/heads/masterreport-status delete-refs side-band-64k quiet atomic ofs-delta agent=git/2.26.0\n0000"
end end
let(:base_headers) do let(:base_headers) do
...@@ -37,6 +37,8 @@ describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -37,6 +37,8 @@ describe Gitlab::Geo::GitSSHProxy, :geo do
} }
end end
let(:irrelevant_encoded_message) { Base64.encode64('irrelevant')}
context 'instance methods' do context 'instance methods' do
subject { described_class.new(data) } subject { described_class.new(data) }
...@@ -47,10 +49,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -47,10 +49,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do
allow(base_request).to receive(:authorization).and_return('secret') allow(base_request).to receive(:authorization).and_return('secret')
end end
describe '#info_refs_receive_pack' do shared_examples 'must be a secondary' do
context 'against primary node' do
let(:current_node) { primary_node }
it 'raises an exception' do it 'raises an exception' do
expect do expect do
subject.info_refs_receive_pack subject.info_refs_receive_pack
...@@ -58,11 +57,245 @@ describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -58,11 +57,245 @@ describe Gitlab::Geo::GitSSHProxy, :geo do
end end
end end
describe '#info_refs_upload_pack' do
context 'against primary node' do
let(:current_node) { primary_node }
it_behaves_like 'must be a secondary'
end
context 'against a secondary node' do
let(:current_node) { secondary_node }
let(:full_info_refs_upload_pack_url) { "#{primary_repo_http}/info/refs?service=git-upload-pack" }
let(:info_refs_upload_pack_http_body_full) { "001e# service=git-upload-pack\n0000#{info_refs_body_short}" }
context 'authorization header is scoped' do
it 'passes the scope when .info_refs_upload_pack is called' do
expect(Gitlab::Geo::BaseRequest).to receive(:new).with(scope: project.repository.full_path)
subject.info_refs_upload_pack
end
it 'passes the scope when .receive_pack is called' do
expect(Gitlab::Geo::BaseRequest).to receive(:new).with(scope: project.repository.full_path)
subject.receive_pack(info_refs_body_short)
end
end
context 'with a failed response' do
let(:error_msg) { 'execution expired' }
before do
stub_request(:get, full_info_refs_upload_pack_url).to_timeout
end
it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do
expect(subject.info_refs_upload_pack).to be_a(Gitlab::Geo::GitSSHProxy::FailedAPIResponse)
end
it 'has a code of 500' do
expect(subject.info_refs_upload_pack.code).to be(500)
end
it 'has a status of false' do
expect(subject.info_refs_upload_pack.body[:status]).to be_falsey
end
it 'has a messsage' do
expect(subject.info_refs_upload_pack.body[:message]).to eql("Failed to contact primary #{primary_repo_http}\nError: #{error_msg}")
end
it 'has no result' do
expect(subject.info_refs_upload_pack.body[:result]).to be_nil
end
end
context 'with an invalid response' do
let(:error_msg) { 'dial unix /Users/ash/src/gdk/gdk-ee/gitlab.socket: connect: connection refused' }
before do
stub_request(:get, full_info_refs_upload_pack_url).to_return(status: 502, body: error_msg)
end
it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do
expect(subject.info_refs_upload_pack).to be_a(Gitlab::Geo::GitSSHProxy::APIResponse)
end
it 'has a code of 502' do
expect(subject.info_refs_upload_pack.code).to be(502)
end
it 'has a status of false' do
expect(subject.info_refs_upload_pack.body[:status]).to be_falsey
end
it 'has a messsage' do
expect(subject.info_refs_upload_pack.body[:message]).to eql("Failed to contact primary #{primary_repo_http}\nError: #{error_msg}")
end
it 'has no result' do
expect(subject.info_refs_upload_pack.body[:result]).to be_nil
end
end
context 'with a valid response' do
before do
stub_request(:get, full_info_refs_upload_pack_url).to_return(status: 200, body: info_refs_upload_pack_http_body_full)
end
it 'returns a Gitlab::Geo::GitSSHProxy::APIResponse' do
expect(subject.info_refs_upload_pack).to be_a(Gitlab::Geo::GitSSHProxy::APIResponse)
end
it 'has a code of 200' do
expect(subject.info_refs_upload_pack.code).to be(200)
end
it 'has a status of true' do
expect(subject.info_refs_upload_pack.body[:status]).to be_truthy
end
it 'has no messsage' do
expect(subject.info_refs_upload_pack.body[:message]).to be_nil
end
it 'returns a modified body' do
expect(subject.info_refs_upload_pack.body[:result]).to eql(Base64.encode64(info_refs_body_short))
end
end
end
end
describe '#upload_pack' do
context 'against primary node' do
let(:current_node) { primary_node }
it_behaves_like 'must be a secondary'
end
context 'against a secondary node' do
let(:current_node) { secondary_node }
let(:full_git_upload_pack_url) { "#{primary_repo_http}/git-upload-pack" }
let(:upload_pack_headers) do
base_headers.merge(
'Content-Type' => 'application/x-git-upload-pack-request',
'Accept' => 'application/x-git-upload-pack-result'
)
end
context 'with a failed response' do
let(:error_msg) { 'execution expired' }
before do
stub_request(:post, full_git_upload_pack_url).to_timeout
end
it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do
expect(subject.upload_pack(irrelevant_encoded_message)).to be_a(Gitlab::Geo::GitSSHProxy::FailedAPIResponse)
end
it 'has a messsage' do
expect(subject.upload_pack(irrelevant_encoded_message).body[:message]).to eql("Failed to contact primary #{primary_repo_http}\nError: #{error_msg}")
end
it 'has no result' do
expect(subject.upload_pack(irrelevant_encoded_message).body[:result]).to be_nil
end
end
context 'with an invalid response' do
let(:error_msg) { 'dial unix /Users/ash/src/gdk/gdk-ee/gitlab.socket: connect: connection refused' }
before do
stub_request(:post, full_git_upload_pack_url).to_return(status: 502, body: error_msg, headers: upload_pack_headers)
end
it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do
expect(subject.upload_pack(irrelevant_encoded_message)).to be_a(Gitlab::Geo::GitSSHProxy::APIResponse)
end
it 'has a messsage' do
expect(subject.upload_pack(irrelevant_encoded_message).body[:message]).to eql("Failed to contact primary #{primary_repo_http}\nError: #{error_msg}")
end
it 'has no result' do
expect(subject.upload_pack(irrelevant_encoded_message).body[:result]).to be_nil
end
end
context 'with a valid response' do
context 'for a git clone operation' do
let(:base64_encoded_response) { Base64.encode64("0090want 13f347b1231b3120c47b8ca7f06dd8b4e021cf6b multi_ack_detailed side-band-64k thin-pack ofs-delta deepen-since deepen-not agent=git/2.26.0\n0032want 13f347b1231b3120c47b8ca7f06dd8b4e021cf6b\n00000009done\n") }
let(:decoded_response) { "0090want 13f347b1231b3120c47b8ca7f06dd8b4e021cf6b multi_ack_detailed side-band-64k thin-pack ofs-delta deepen-since deepen-not agent=git/2.26.0\n0000\n00000009done\n" }
let(:base64_encoded_expected_body) { Base64.encode64(decoded_response) }
before do
stub_request(:post, full_git_upload_pack_url).to_return(status: 201, body: decoded_response, headers: upload_pack_headers)
end
it 'returns a Gitlab::Geo::GitSSHProxy::APIResponse' do
expect(subject.upload_pack(base64_encoded_response)).to be_a(Gitlab::Geo::GitSSHProxy::APIResponse)
end
it 'has a code of 201' do
expect(subject.upload_pack(base64_encoded_response).code).to be(201)
end
it 'has no messsage' do
expect(subject.upload_pack(base64_encoded_response).body[:message]).to be_nil
end
it 'has a result' do
expect(subject.upload_pack(base64_encoded_response).body[:result]).to eql(base64_encoded_expected_body)
end
end
context 'for a git pull operation' do
let(:base64_encoded_response) { Base64.encode64("009cwant af3551b2213219f07ab3adaa4bbd22c7c2638010 multi_ack_detailed side-band-64k thin-pack include-tag ofs-delta deepen-since deepen-not agent=git/2.26.0\n00000032have 13f347b1231b3120c47b8ca7f06dd8b4e021cf6b\n0032have 8195a05c3707e28af2ad4d3512f0fdee4c0bd3ee\n0032have 11f60ba825dbe91eebb5ea1701e3b404c0409e21\n0032have a1f474b6173894844dccb70634d8a593f9d0122f\n0032have b6bd59aa5e9511f6685d5f5e362344f74cb8bd9c\n0032have 1caf913d6fb1acbbed004242bb2455dc67ababd9\n0032have 8a84d3ef290f6d0e5060ecbd2f7a5ffb914b2a6b\n0032have 46ad1c9f39ee9ed35e473263b51ec0522d392a3f\n0032have 16c89af2a83438854c438fb5142493c9fdf96449\n0032have 19fff49349aa6d3a74120182b849a5bf7f3962d8\n0032have db70951afb1563340490f720638ce84e13efb186\n0032have 022fbf7856bffb6b090ac818a23d1ea3e77b4609\n0032have e45b528d0fa7ea8af0085ceb90beff01cd1681e4\n0032have 0b2c3606420b8b511e7761177d30066a11350460\n0032have 440a9dfb6dd7dd8bb5a577026492c9a68aad7f2a\n0032have 71e5d9ef406fae17f944a956dc68b078ddffb65d\n0000") }
let(:decoded_response) { "009cwant af3551b2213219f07ab3adaa4bbd22c7c2638010 multi_ack_detailed side-band-64k thin-pack include-tag ofs-delta deepen-since deepen-not agent=git/2.26.0\n00000032have 13f347b1231b3120c47b8ca7f06dd8b4e021cf6b\n0032have 8195a05c3707e28af2ad4d3512f0fdee4c0bd3ee\n0032have 11f60ba825dbe91eebb5ea1701e3b404c0409e21\n0032have a1f474b6173894844dccb70634d8a593f9d0122f\n0032have b6bd59aa5e9511f6685d5f5e362344f74cb8bd9c\n0032have 1caf913d6fb1acbbed004242bb2455dc67ababd9\n0032have 8a84d3ef290f6d0e5060ecbd2f7a5ffb914b2a6b\n0032have 46ad1c9f39ee9ed35e473263b51ec0522d392a3f\n0032have 16c89af2a83438854c438fb5142493c9fdf96449\n0032have 19fff49349aa6d3a74120182b849a5bf7f3962d8\n0032have db70951afb1563340490f720638ce84e13efb186\n0032have 022fbf7856bffb6b090ac818a23d1ea3e77b4609\n0032have e45b528d0fa7ea8af0085ceb90beff01cd1681e4\n0032have 0b2c3606420b8b511e7761177d30066a11350460\n0032have 440a9dfb6dd7dd8bb5a577026492c9a68aad7f2a\n0032have 71e5d9ef406fae17f944a956dc68b078ddffb65d\n0009done\n0000" }
let(:base64_encoded_expected_body) { Base64.encode64(decoded_response) }
before do
stub_request(:post, full_git_upload_pack_url).to_return(status: 201, body: decoded_response, headers: upload_pack_headers)
end
it 'returns a Gitlab::Geo::GitSSHProxy::APIResponse' do
expect(subject.upload_pack(base64_encoded_response)).to be_a(Gitlab::Geo::GitSSHProxy::APIResponse)
end
it 'has a code of 201' do
expect(subject.upload_pack(base64_encoded_response).code).to be(201)
end
it 'has no messsage' do
expect(subject.upload_pack(base64_encoded_response).body[:message]).to be_nil
end
it 'has a result' do
expect(subject.upload_pack(base64_encoded_response).body[:result]).to eql(base64_encoded_expected_body)
end
end
end
end
end
describe '#info_refs_receive_pack' do
context 'against primary node' do
let(:current_node) { primary_node }
it_behaves_like 'must be a secondary'
end
context 'against secondary node' do context 'against secondary node' do
let(:current_node) { secondary_node } let(:current_node) { secondary_node }
let(:full_info_refs_url) { "#{primary_repo_http}/info/refs?service=git-receive-pack" } let(:full_info_refs_receive_pack_url) { "#{primary_repo_http}/info/refs?service=git-receive-pack" }
let(:info_refs_http_body_full) { "001f# service=git-receive-pack\n0000#{info_refs_body_short}" } let(:info_refs_receive_pack_http_body_full) { "001f# service=git-receive-pack\n0000#{info_refs_body_short}" }
context 'authorization header is scoped' do context 'authorization header is scoped' do
it 'passes the scope when .info_refs_receive_pack is called' do it 'passes the scope when .info_refs_receive_pack is called' do
...@@ -82,7 +315,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -82,7 +315,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do
let(:error_msg) { 'execution expired' } let(:error_msg) { 'execution expired' }
before do before do
stub_request(:get, full_info_refs_url).to_timeout stub_request(:get, full_info_refs_receive_pack_url).to_timeout
end end
it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do
...@@ -110,7 +343,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -110,7 +343,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do
let(:error_msg) { 'dial unix /Users/ash/src/gdk/gdk-ee/gitlab.socket: connect: connection refused' } let(:error_msg) { 'dial unix /Users/ash/src/gdk/gdk-ee/gitlab.socket: connect: connection refused' }
before do before do
stub_request(:get, full_info_refs_url).to_return(status: 502, body: error_msg) stub_request(:get, full_info_refs_receive_pack_url).to_return(status: 502, body: error_msg)
end end
it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do
...@@ -136,7 +369,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -136,7 +369,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do
context 'with a valid response' do context 'with a valid response' do
before do before do
stub_request(:get, full_info_refs_url).to_return(status: 200, body: info_refs_http_body_full) stub_request(:get, full_info_refs_receive_pack_url).to_return(status: 200, body: info_refs_receive_pack_http_body_full)
end end
it 'returns a Gitlab::Geo::GitSSHProxy::APIResponse' do it 'returns a Gitlab::Geo::GitSSHProxy::APIResponse' do
...@@ -166,11 +399,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -166,11 +399,7 @@ describe Gitlab::Geo::GitSSHProxy, :geo do
context 'against primary node' do context 'against primary node' do
let(:current_node) { primary_node } let(:current_node) { primary_node }
it 'raises an exception' do it_behaves_like 'must be a secondary'
expect do
subject.receive_pack(info_refs_body_short)
end.to raise_error(described_class::MustBeASecondaryNode)
end
end end
context 'against secondary node' do context 'against secondary node' do
...@@ -192,15 +421,15 @@ describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -192,15 +421,15 @@ describe Gitlab::Geo::GitSSHProxy, :geo do
end end
it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do
expect(subject.receive_pack(info_refs_body_short)).to be_a(Gitlab::Geo::GitSSHProxy::FailedAPIResponse) expect(subject.receive_pack(irrelevant_encoded_message)).to be_a(Gitlab::Geo::GitSSHProxy::FailedAPIResponse)
end end
it 'has a messsage' do it 'has a messsage' do
expect(subject.receive_pack(info_refs_body_short).body[:message]).to eql("Failed to contact primary #{primary_repo_http}\nError: #{error_msg}") expect(subject.receive_pack(irrelevant_encoded_message).body[:message]).to eql("Failed to contact primary #{primary_repo_http}\nError: #{error_msg}")
end end
it 'has no result' do it 'has no result' do
expect(subject.receive_pack(info_refs_body_short).body[:result]).to be_nil expect(subject.receive_pack(irrelevant_encoded_message).body[:result]).to be_nil
end end
end end
...@@ -212,40 +441,42 @@ describe Gitlab::Geo::GitSSHProxy, :geo do ...@@ -212,40 +441,42 @@ describe Gitlab::Geo::GitSSHProxy, :geo do
end end
it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do it 'returns a Gitlab::Geo::GitSSHProxy::FailedAPIResponse' do
expect(subject.receive_pack(info_refs_body_short)).to be_a(Gitlab::Geo::GitSSHProxy::APIResponse) expect(subject.receive_pack(irrelevant_encoded_message)).to be_a(Gitlab::Geo::GitSSHProxy::APIResponse)
end end
it 'has a messsage' do it 'has a messsage' do
expect(subject.receive_pack(info_refs_body_short).body[:message]).to eql("Failed to contact primary #{primary_repo_http}\nError: #{error_msg}") expect(subject.receive_pack(irrelevant_encoded_message).body[:message]).to eql("Failed to contact primary #{primary_repo_http}\nError: #{error_msg}")
end end
it 'has no result' do it 'has no result' do
expect(subject.receive_pack(info_refs_body_short).body[:result]).to be_nil expect(subject.receive_pack(irrelevant_encoded_message).body[:result]).to be_nil
end end
end end
context 'with a valid response' do context 'with a valid response' do
let(:body) { '<binary content>' } let(:decoded_response) { "0095bc3b8ba91de3ecd161440326eda0b89a3c91d339 fc02c3ed8ef0b2dc7cc6e3e3bcf6b13465fea91b refs/heads/master report-status side-band-64k agent=git/2.26.00000PACK<binary>" }
let(:base64_encoded_body) { Base64.encode64(body) } let(:base64_encoded_response) { decoded_response }
let(:base64_encoded_expected_body) { Base64.encode64(decoded_response) }
before do before do
stub_request(:post, full_git_receive_pack_url).to_return(status: 201, body: body, headers: receive_pack_headers) stub_request(:post, full_git_receive_pack_url).to_return(status: 201, body: decoded_response, headers: receive_pack_headers)
end end
it 'returns a Gitlab::Geo::GitSSHProxy::APIResponse' do it 'returns a Gitlab::Geo::GitSSHProxy::APIResponse' do
expect(subject.receive_pack(info_refs_body_short)).to be_a(Gitlab::Geo::GitSSHProxy::APIResponse) expect(subject.receive_pack(base64_encoded_response)).to be_a(Gitlab::Geo::GitSSHProxy::APIResponse)
end end
it 'has a code of 201' do it 'has a code of 201' do
expect(subject.receive_pack(info_refs_body_short).code).to be(201) expect(subject.receive_pack(base64_encoded_response).code).to be(201)
end end
it 'has no messsage' do it 'has no messsage' do
expect(subject.receive_pack(info_refs_body_short).body[:message]).to be_nil expect(subject.receive_pack(base64_encoded_response).body[:message]).to be_nil
end end
it 'has a result' do it 'has a result' do
expect(subject.receive_pack(info_refs_body_short).body[:result]).to eql(base64_encoded_body) expect(subject.receive_pack(base64_encoded_response).body[:result]).to eql(base64_encoded_expected_body)
end end
end end
end end
......
...@@ -331,6 +331,7 @@ describe Gitlab::GitAccess do ...@@ -331,6 +331,7 @@ describe Gitlab::GitAccess do
end end
end end
context 'for a repository that has been replicated' do
context 'that has no DB replication lag' do context 'that has no DB replication lag' do
let(:current_replication_lag) { 0 } let(:current_replication_lag) { 0 }
...@@ -347,10 +348,50 @@ describe Gitlab::GitAccess do ...@@ -347,10 +348,50 @@ describe Gitlab::GitAccess do
end end
end end
end end
context 'for a repository that has yet to be replicated' do
let(:project) { create(:project) }
let(:current_replication_lag) { 0 }
before do
create(:geo_node, :primary)
end
it 'returns a custom action' do
expected_payload = { "action" => "geo_proxy_to_primary", "data" => { "api_endpoints" => ["/api/v4/geo/proxy_git_ssh/info_refs_upload_pack", "/api/v4/geo/proxy_git_ssh/upload_pack"], "primary_repo" => geo_primary_http_url_to_repo(project) } }
expected_console_messages = ["This request to a Geo secondary node will be forwarded to the", "Geo primary node:", "", " #{geo_primary_ssh_url_to_repo(project)}"]
response = pull_changes
expect(response).to be_instance_of(Gitlab::GitAccessResult::CustomAction)
expect(response.payload).to eq(expected_payload)
expect(response.console_messages).to eq(expected_console_messages)
end
end
end
end end
context 'git push' do context 'git push' do
it { expect { push_changes }.to raise_forbidden(Gitlab::GitAccess::ERROR_MESSAGES[:upload]) } it { expect { push_changes }.to raise_forbidden(Gitlab::GitAccess::ERROR_MESSAGES[:upload]) }
context 'for a secondary' do
before do
stub_licensed_features(geo: true)
create(:geo_node, :primary)
stub_current_geo_node(create(:geo_node))
end
it 'returns a custom action' do
expected_payload = { "action" => "geo_proxy_to_primary", "data" => { "api_endpoints" => ["/api/v4/geo/proxy_git_ssh/info_refs_receive_pack", "/api/v4/geo/proxy_git_ssh/receive_pack"], "primary_repo" => geo_primary_http_url_to_repo(project) } }
expected_console_messages = ["This request to a Geo secondary node will be forwarded to the", "Geo primary node:", "", " #{geo_primary_ssh_url_to_repo(project)}"]
response = push_changes
expect(response).to be_instance_of(Gitlab::GitAccessResult::CustomAction)
expect(response.payload).to eq(expected_payload)
expect(response.console_messages).to eq(expected_console_messages)
end
end
end end
end end
......
...@@ -39,6 +39,10 @@ describe Gitlab::GitAccessWiki do ...@@ -39,6 +39,10 @@ describe Gitlab::GitAccessWiki do
private private
def pull_changes(changes = Gitlab::GitAccess::ANY)
access.check('git-upload-pack', changes)
end
def push_changes(changes = Gitlab::GitAccess::ANY) def push_changes(changes = Gitlab::GitAccess::ANY)
access.check('git-receive-pack', changes) access.check('git-receive-pack', changes)
end end
......
...@@ -364,6 +364,126 @@ describe API::Geo do ...@@ -364,6 +364,126 @@ describe API::Geo do
stub_current_geo_node(secondary_node) stub_current_geo_node(secondary_node)
end end
describe 'POST /geo/proxy_git_ssh/info_refs_upload_pack' do
context 'with all required params missing' do
it 'responds with 400' do
post api('/geo/proxy_git_ssh/info_refs_upload_pack'), params: nil
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eql('secret_token is missing, data is missing, data[gl_id] is missing, data[primary_repo] is missing')
end
end
context 'with all required params' do
let(:git_push_ssh_proxy) { double(Gitlab::Geo::GitSSHProxy) }
before do
allow(Gitlab::Geo::GitSSHProxy).to receive(:new).with(data).and_return(git_push_ssh_proxy)
end
context 'with an invalid secret_token' do
it 'responds with 401' do
post(api('/geo/proxy_git_ssh/info_refs_upload_pack'), params: { secret_token: 'invalid', data: data })
expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response['error']).to be_nil
end
end
context 'where an exception occurs' do
it 'responds with 500' do
expect(git_push_ssh_proxy).to receive(:info_refs_upload_pack).and_raise('deliberate exception raised')
post api('/geo/proxy_git_ssh/info_refs_upload_pack'), params: { secret_token: secret_token, data: data }
expect(response).to have_gitlab_http_status(:internal_server_error)
expect(json_response['message']).to include('RuntimeError (deliberate exception raised)')
expect(json_response['result']).to be_nil
end
end
context 'with a valid secret token' do
let(:http_response) { double(Net::HTTPOK, code: 200, body: 'something here') }
let(:api_response) { Gitlab::Geo::GitSSHProxy::APIResponse.from_http_response(http_response, primary_repo) }
before do
# Mocking a real Net::HTTPSuccess is very difficult as it's not
# easy to instantiate the class due to the way it sets the body
expect(http_response).to receive(:is_a?).with(Net::HTTPSuccess).and_return(true)
end
it 'responds with 200' do
expect(git_push_ssh_proxy).to receive(:info_refs_upload_pack).and_return(api_response)
post api('/geo/proxy_git_ssh/info_refs_upload_pack'), params: { secret_token: secret_token, data: data }
expect(response).to have_gitlab_http_status(:ok)
expect(Base64.decode64(json_response['result'])).to eql('something here')
end
end
end
end
describe 'POST /geo/proxy_git_ssh/upload_pack' do
context 'with all required params missing' do
it 'responds with 400' do
post api('/geo/proxy_git_ssh/upload_pack'), params: nil
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eql('secret_token is missing, data is missing, data[gl_id] is missing, data[primary_repo] is missing, output is missing')
end
end
context 'with all required params' do
let(:output) { Base64.encode64('info_refs content') }
let(:git_push_ssh_proxy) { double(Gitlab::Geo::GitSSHProxy) }
before do
allow(Gitlab::Geo::GitSSHProxy).to receive(:new).with(data).and_return(git_push_ssh_proxy)
end
context 'with an invalid secret_token' do
it 'responds with 401' do
post(api('/geo/proxy_git_ssh/upload_pack'), params: { secret_token: 'invalid', data: data, output: output })
expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response['error']).to be_nil
end
end
context 'where an exception occurs' do
it 'responds with 500' do
expect(git_push_ssh_proxy).to receive(:upload_pack).and_raise('deliberate exception raised')
post api('/geo/proxy_git_ssh/upload_pack'), params: { secret_token: secret_token, data: data, output: output }
expect(response).to have_gitlab_http_status(:internal_server_error)
expect(json_response['message']).to include('RuntimeError (deliberate exception raised)')
expect(json_response['result']).to be_nil
end
end
context 'with a valid secret token' do
let(:http_response) { double(Net::HTTPCreated, code: 201, body: 'something here', class: Net::HTTPCreated) }
let(:api_response) { Gitlab::Geo::GitSSHProxy::APIResponse.from_http_response(http_response, primary_repo) }
before do
# Mocking a real Net::HTTPSuccess is very difficult as it's not
# easy to instantiate the class due to the way it sets the body
expect(http_response).to receive(:is_a?).with(Net::HTTPSuccess).and_return(true)
end
it 'responds with 201' do
expect(git_push_ssh_proxy).to receive(:upload_pack).with(output).and_return(api_response)
post api('/geo/proxy_git_ssh/upload_pack'), params: { secret_token: secret_token, data: data, output: output }
expect(response).to have_gitlab_http_status(:created)
expect(Base64.decode64(json_response['result'])).to eql('something here')
end
end
end
end
describe 'POST /geo/proxy_git_ssh/info_refs_receive_pack' do describe 'POST /geo/proxy_git_ssh/info_refs_receive_pack' do
context 'with all required params missing' do context 'with all required params missing' do
it 'responds with 400' do it 'responds with 400' do
......
...@@ -28,22 +28,44 @@ RSpec.shared_examples 'a read-only GitLab instance' do ...@@ -28,22 +28,44 @@ RSpec.shared_examples 'a read-only GitLab instance' do
context 'that is correctly set up' do context 'that is correctly set up' do
let(:secondary_with_primary) { true } let(:secondary_with_primary) { true }
let(:console_messages) do
[
"This request to a Geo secondary node will be forwarded to the",
"Geo primary node:",
"",
" #{primary_repo_ssh_url}"
]
end
context 'for a git clone/pull' do
let(:payload) do let(:payload) do
{ {
'action' => 'geo_proxy_to_primary', 'action' => 'geo_proxy_to_primary',
'data' => { 'data' => {
'api_endpoints' => %w{/api/v4/geo/proxy_git_ssh/info_refs_receive_pack /api/v4/geo/proxy_git_ssh/receive_pack}, 'api_endpoints' => %w{/api/v4/geo/proxy_git_ssh/info_refs_upload_pack /api/v4/geo/proxy_git_ssh/upload_pack},
'primary_repo' => primary_repo_url 'primary_repo' => primary_repo_url
} }
} }
end end
let(:console_messages) do
[ it 'attempts to proxy to the primary' do
"This request to a Geo secondary node will be forwarded to the", project.add_maintainer(user)
"Geo primary node:",
"", expect(pull_changes).to be_a(Gitlab::GitAccessResult::CustomAction)
" #{primary_repo_ssh_url}" expect(pull_changes.payload).to eql(payload)
] expect(pull_changes.console_messages).to include(*console_messages)
end
end
context 'for a git push' do
let(:payload) do
{
'action' => 'geo_proxy_to_primary',
'data' => {
'api_endpoints' => %w{/api/v4/geo/proxy_git_ssh/info_refs_receive_pack /api/v4/geo/proxy_git_ssh/receive_pack},
'primary_repo' => primary_repo_url
}
}
end end
it 'attempts to proxy to the primary' do it 'attempts to proxy to the primary' do
...@@ -55,4 +77,5 @@ RSpec.shared_examples 'a read-only GitLab instance' do ...@@ -55,4 +77,5 @@ RSpec.shared_examples 'a read-only GitLab instance' do
end end
end end
end 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