Commit 5f12a6a4 authored by Ash McKenzie's avatar Ash McKenzie Committed by Nick Thomas

Display helpful feedback when proxying an SSH git push to secondary request (v2)

parent dd54651b
...@@ -22,20 +22,15 @@ module Action ...@@ -22,20 +22,15 @@ module Action
def execute def execute
validate! validate!
result = process_api_endpoints inform_client(info_message) if info_message
process_api_endpoints!
if result && HTTP_SUCCESS_CODES.include?(result.code)
result
else
raise_unsuccessful!(result)
end
end end
private private
attr_reader :gl_id, :payload attr_reader :gl_id, :payload
def process_api_endpoints def process_api_endpoints!
output = '' output = ''
resp = nil resp = nil
...@@ -46,7 +41,14 @@ module Action ...@@ -46,7 +41,14 @@ module Action
json = { 'data' => data_with_gl_id, 'output' => output } json = { 'data' => data_with_gl_id, 'output' => output }
resp = post(url, {}, headers: DEFAULT_HEADERS, options: { json: json }) resp = post(url, {}, headers: DEFAULT_HEADERS, options: { json: json })
return resp unless HTTP_SUCCESS_CODES.include?(resp.code)
# Net::HTTPSuccess is the parent of Net::HTTPOK, Net::HTTPCreated etc.
case resp
when Net::HTTPSuccess, Net::HTTPMultipleChoices
true
else
raise_unsuccessful!(resp)
end
begin begin
body = JSON.parse(resp.body) body = JSON.parse(resp.body)
...@@ -54,8 +56,6 @@ module Action ...@@ -54,8 +56,6 @@ module Action
raise UnsuccessfulError, 'Response was not valid JSON' raise UnsuccessfulError, 'Response was not valid JSON'
end end
inform_client(body['message']) if body['message']
print_flush(body['result']) print_flush(body['result'])
# In the context of the git push sequence of events, it's necessary to read # In the context of the git push sequence of events, it's necessary to read
...@@ -78,6 +78,10 @@ module Action ...@@ -78,6 +78,10 @@ module Action
data['api_endpoints'] data['api_endpoints']
end end
def info_message
data['info_message']
end
def config def config
@config ||= GitlabConfig.new @config ||= GitlabConfig.new
end end
...@@ -97,7 +101,11 @@ module Action ...@@ -97,7 +101,11 @@ module Action
end end
def inform_client(str) def inform_client(str)
$stderr.puts(str) $stderr.puts(format_gitlab_output(str))
end
def format_gitlab_output(str)
str.split("\n").map { |line| "> GitLab: #{line}" }.join("\n")
end end
def validate! def validate!
...@@ -120,16 +128,17 @@ module Action ...@@ -120,16 +128,17 @@ module Action
end end
def raise_unsuccessful!(result) def raise_unsuccessful!(result)
message = begin message = "#{exception_message_for(result.body)} (#{result.code})"
body = JSON.parse(result.body) raise UnsuccessfulError, format_gitlab_output(message)
message = body['message'] end
message = Base64.decode64(body['result']) if !message && body['result'] && !body['result'].empty?
message ? message : NO_MESSAGE_TEXT def exception_message_for(body)
rescue JSON::ParserError body = JSON.parse(body)
NO_MESSAGE_TEXT return body['message'] unless body['message'].to_s.empty?
end
raise UnsuccessfulError, "#{message} (#{result.code})" body['result'].to_s.empty? ? NO_MESSAGE_TEXT : Base64.decode64(body['result'])
rescue JSON::ParserError
NO_MESSAGE_TEXT
end end
end end
end end
require 'json' require 'json'
require_relative 'http_codes'
class GitAccessStatus class GitAccessStatus
include HTTPCodes HTTP_MULTIPLE_CHOICES = '300'.freeze
attr_reader :message, :gl_repository, :gl_id, :gl_username, :gitaly, :git_protocol, :git_config_options, :payload attr_reader :message, :gl_repository, :gl_id, :gl_username, :gitaly, :git_protocol, :git_config_options, :payload
......
...@@ -95,8 +95,9 @@ class GitlabShell # rubocop:disable Metrics/ClassLength ...@@ -95,8 +95,9 @@ class GitlabShell # rubocop:disable Metrics/ClassLength
$stderr.puts "GitLab: Invalid repository path" $stderr.puts "GitLab: Invalid repository path"
false false
rescue Action::Custom::BaseError => ex rescue Action::Custom::BaseError => ex
$logger.warn('Custom action error', command: origin_cmd, user: log_username) $logger.warn('Custom action error', exception: ex.class, message: ex.message,
$stderr.puts "GitLab: #{ex.message}" command: origin_cmd, user: log_username)
$stderr.puts ex.message
false false
end end
......
module HTTPCodes
HTTP_SUCCESS = '200'.freeze
HTTP_CREATED = '201'.freeze
HTTP_MULTIPLE_CHOICES = '300'.freeze
HTTP_UNAUTHORIZED = '401'.freeze
HTTP_NOT_FOUND = '404'.freeze
HTTP_SUCCESS_CODES = [HTTP_SUCCESS, HTTP_CREATED, HTTP_MULTIPLE_CHOICES].freeze
end
require_relative 'http_codes'
require_relative 'httpunix' require_relative 'httpunix'
require_relative 'gitlab_logger' require_relative 'gitlab_logger'
module HTTPHelper module HTTPHelper
include HTTPCodes
READ_TIMEOUT = 300 READ_TIMEOUT = 300
CONTENT_TYPE_JSON = 'application/json'.freeze CONTENT_TYPE_JSON = 'application/json'.freeze
......
...@@ -26,62 +26,57 @@ describe Action::Custom do ...@@ -26,62 +26,57 @@ describe Action::Custom do
end end
context 'that are valid' do context 'that are valid' do
where(:primary_repo_data) do let(:payload) do
[ {
[ 'http://localhost:3001/user1/repo1.git' ], 'action' => 'geo_proxy_to_primary',
[{ 'http' => 'http://localhost:3001/user1/repo1.git' }], 'data' => {
[{ 'http' => 'http://localhost:3001/user1/repo1.git', 'ssh' => 'ssh://user@localhost:3002/user1/repo1.git' }] 'api_endpoints' => %w{/api/v4/fake/info_refs /api/v4/fake/push},
] 'primary_repo' => 'http://localhost:3001/user1/repo1.git'
}
}
end end
with_them do context 'and responds correctly' do
let(:payload) do it 'prints a Base64 encoded result to $stdout' do
{ VCR.use_cassette("custom-action-ok") do
'action' => 'geo_proxy_to_primary', expect($stdout).to receive(:print).with('info_refs-result').ordered
'data' => { expect($stdout).to receive(:print).with('push-result').ordered
'api_endpoints' => %w{/api/v4/fake/info_refs /api/v4/fake/push}, subject.execute
'gl_username' => 'user1', end
'primary_repo' => primary_repo_data
}
}
end end
context 'and responds correctly' do context 'with results printed to $stdout' do
it 'prints a Base64 encoded result to $stdout' do before do
allow($stdout).to receive(:print).with('info_refs-result')
allow($stdout).to receive(:print).with('push-result')
end
it 'returns an instance of Net::HTTPCreated' do
VCR.use_cassette("custom-action-ok") do VCR.use_cassette("custom-action-ok") do
expect($stdout).to receive(:print).with('info_refs-result').ordered expect(subject.execute).to be_instance_of(Net::HTTPCreated)
expect($stdout).to receive(:print).with('push-result').ordered
subject.execute
end end
end end
context 'with results printed to $stdout' do context 'and with an information message provided' do
before do before do
allow($stdout).to receive(:print).with('info_refs-result') payload['data']['info_message'] = 'Important message here.'
allow($stdout).to receive(:print).with('push-result')
end end
it 'prints a message to $stderr' do it 'prints said informational message to $stderr' do
VCR.use_cassette("custom-action-ok-with-message") do VCR.use_cassette("custom-action-ok-with-message") do
expect { subject.execute }.to output(/NOTE: Message here/).to_stderr expect { subject.execute }.to output(/Important message here./).to_stderr
end
end
it 'returns an instance of Net::HTTPCreated' do
VCR.use_cassette("custom-action-ok") do
expect(subject.execute ).to be_instance_of(Net::HTTPCreated)
end end
end end
end end
end end
end
context 'but responds incorrectly' do context 'but responds incorrectly' do
it 'raises an UnsuccessfulError exception' do it 'raises an UnsuccessfulError exception' do
VCR.use_cassette("custom-action-ok-not-json") do VCR.use_cassette("custom-action-ok-not-json") do
expect { expect do
subject.execute subject.execute
}.to raise_error(Action::Custom::UnsuccessfulError, 'Response was not valid JSON') end.to raise_error(Action::Custom::UnsuccessfulError, 'Response was not valid JSON')
end
end end
end end
end end
...@@ -93,7 +88,6 @@ describe Action::Custom do ...@@ -93,7 +88,6 @@ describe Action::Custom do
{ {
'action' => 'geo_proxy_to_primary', 'action' => 'geo_proxy_to_primary',
'data' => { 'data' => {
'gl_username' => 'user1',
'primary_repo' => 'http://localhost:3001/user1/repo1.git' 'primary_repo' => 'http://localhost:3001/user1/repo1.git'
} }
} }
...@@ -110,7 +104,6 @@ describe Action::Custom do ...@@ -110,7 +104,6 @@ describe Action::Custom do
'action' => 'geo_proxy_to_primary', 'action' => 'geo_proxy_to_primary',
'data' => { 'data' => {
'api_endpoints' => [], 'api_endpoints' => [],
'gl_username' => 'user1',
'primary_repo' => 'http://localhost:3001/user1/repo1.git' 'primary_repo' => 'http://localhost:3001/user1/repo1.git'
} }
} }
...@@ -135,7 +128,6 @@ describe Action::Custom do ...@@ -135,7 +128,6 @@ describe Action::Custom do
'action' => 'geo_proxy_to_primary', 'action' => 'geo_proxy_to_primary',
'data' => { 'data' => {
'api_endpoints' => %w{/api/v4/fake/info_refs_bad /api/v4/fake/push_bad}, 'api_endpoints' => %w{/api/v4/fake/info_refs_bad /api/v4/fake/push_bad},
'gl_username' => 'user1',
'primary_repo' => 'http://localhost:3001/user1/repo1.git' 'primary_repo' => 'http://localhost:3001/user1/repo1.git'
} }
} }
...@@ -144,9 +136,9 @@ describe Action::Custom do ...@@ -144,9 +136,9 @@ describe Action::Custom do
context 'and response is JSON' do context 'and response is JSON' do
it 'raises an UnsuccessfulError exception' do it 'raises an UnsuccessfulError exception' do
VCR.use_cassette("custom-action-not-ok-json") do VCR.use_cassette("custom-action-not-ok-json") do
expect { expect do
subject.execute subject.execute
}.to raise_error(Action::Custom::UnsuccessfulError, 'You cannot perform write operations on a read-only instance (403)') end.to raise_error(Action::Custom::UnsuccessfulError, '> GitLab: You cannot perform write operations on a read-only instance (403)')
end end
end end
end end
...@@ -154,9 +146,9 @@ describe Action::Custom do ...@@ -154,9 +146,9 @@ describe Action::Custom do
context 'and response is not JSON' do context 'and response is not JSON' do
it 'raises an UnsuccessfulError exception' do it 'raises an UnsuccessfulError exception' do
VCR.use_cassette("custom-action-not-ok-not-json") do VCR.use_cassette("custom-action-not-ok-not-json") do
expect { expect do
subject.execute subject.execute
}.to raise_error(Action::Custom::UnsuccessfulError, 'No message (403)') end.to raise_error(Action::Custom::UnsuccessfulError, '> GitLab: No message (403)')
end end
end end
end end
......
...@@ -8,7 +8,7 @@ describe GitlabAccess do ...@@ -8,7 +8,7 @@ describe GitlabAccess do
let(:api) do let(:api) do
double(GitlabNet).tap do |api| double(GitlabNet).tap do |api|
allow(api).to receive(:check_access).and_return(GitAccessStatus.new(true, allow(api).to receive(:check_access).and_return(GitAccessStatus.new(true,
HTTPCodes::HTTP_SUCCESS, '200',
'ok', 'ok',
gl_repository: 'project-1', gl_repository: 'project-1',
gl_id: 'user-123', gl_id: 'user-123',
...@@ -46,7 +46,7 @@ describe GitlabAccess do ...@@ -46,7 +46,7 @@ describe GitlabAccess do
before do before do
allow(api).to receive(:check_access).and_return(GitAccessStatus.new( allow(api).to receive(:check_access).and_return(GitAccessStatus.new(
false, false,
HTTPCodes::HTTP_UNAUTHORIZED, '401',
'denied', 'denied',
gl_repository: nil, gl_repository: nil,
gl_id: nil, gl_id: nil,
......
...@@ -25,7 +25,7 @@ describe GitlabShell do ...@@ -25,7 +25,7 @@ describe GitlabShell do
let(:gitaly_check_access) do let(:gitaly_check_access) do
GitAccessStatus.new( GitAccessStatus.new(
true, true,
HTTPCodes::HTTP_SUCCESS, '200',
'ok', 'ok',
gl_repository: gl_repository, gl_repository: gl_repository,
gl_id: gl_id, gl_id: gl_id,
...@@ -41,7 +41,7 @@ describe GitlabShell do ...@@ -41,7 +41,7 @@ describe GitlabShell do
allow(api).to receive(:discover).and_return({ 'name' => 'John Doe', 'username' => 'testuser' }) allow(api).to receive(:discover).and_return({ 'name' => 'John Doe', 'username' => 'testuser' })
allow(api).to receive(:check_access).and_return(GitAccessStatus.new( allow(api).to receive(:check_access).and_return(GitAccessStatus.new(
true, true,
HTTPCodes::HTTP_SUCCESS, '200',
'ok', 'ok',
gl_repository: gl_repository, gl_repository: gl_repository,
gl_id: gl_id, gl_id: gl_id,
...@@ -262,7 +262,7 @@ describe GitlabShell do ...@@ -262,7 +262,7 @@ describe GitlabShell do
let(:custom_action_gitlab_access_status) do let(:custom_action_gitlab_access_status) do
GitAccessStatus.new( GitAccessStatus.new(
true, true,
HTTPCodes::HTTP_MULTIPLE_CHOICES, '300',
'Multiple Choices', 'Multiple Choices',
payload: fake_payload payload: fake_payload
) )
......
...@@ -46,7 +46,7 @@ http_interactions: ...@@ -46,7 +46,7 @@ http_interactions:
- '1.436040' - '1.436040'
body: body:
encoding: UTF-8 encoding: UTF-8
string: '{"result":"aW5mb19yZWZzLXJlc3VsdA==\n", "message":"NOTE: Message here"}' string: '{"result":"aW5mb19yZWZzLXJlc3VsdA==\n"}'
http_version: http_version:
recorded_at: Fri, 20 Jul 2018 06:18:58 GMT recorded_at: Fri, 20 Jul 2018 06:18:58 GMT
- request: - request:
......
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