Commit e0824f17 authored by Jacob Vosmaer's avatar Jacob Vosmaer Committed by Nick Thomas

Remove hooks, they belong to Gitaly now

parent 433cc965
......@@ -20,7 +20,7 @@ An overview of the four cases described above:
## Git hooks
For historical reasons the gitlab-shell repository also contains the
The gitlab-shell repository used to also contain the
Git hooks that allow GitLab to validate Git pushes (e.g. "is this user
allowed to push to this protected branch"). These hooks also trigger
events in GitLab (e.g. to start a CI pipeline after a push).
......@@ -30,17 +30,13 @@ require direct disk access to Git repositories, and that is only
possible on Gitaly servers. It makes no sense to have to install
gitlab-shell on Gitaly servers.
As of GitLab 11.9 [the actual Git hooks are in the Gitaly
As of GitLab 11.10 [the actual Git hooks are in the Gitaly
repository](https://gitlab.com/gitlab-org/gitaly/tree/v1.22.0/ruby/vendor/gitlab-shell/hooks),
but gitlab-shell must still be installed on Gitaly servers because the
hooks rely on configuration data (e.g. the GitLab internal API URL) that
is not yet available in Gitaly itself. Also see the [transition
plan](https://gitlab.com/gitlab-org/gitaly/issues/1226#note_126519133).
This means that for GitLab 11.9 and up, it is pointless to make changes
to Git hook code in the gitlab-shell repository, because the code that
gets run is in the Gitaly repository instead.
## Code status
[![pipeline status](https://gitlab.com/gitlab-org/gitlab-shell/badges/master/pipeline.svg)](https://gitlab.com/gitlab-org/gitlab-shell/commits/master)
......
#!/usr/bin/env ruby
#!/bin/sh
echo "The gitlab-shell hooks have been migrated to Gitaly, see https://gitlab.com/gitlab-org/gitaly/issues/1226"
exit 1
# This file was placed here by GitLab. It makes sure that your pushed commits
# will be processed properly.
refs = $stdin.read
key_id = ENV.delete('GL_ID')
gl_repository = ENV['GL_REPOSITORY']
repo_path = Dir.pwd
require_relative '../lib/gitlab_custom_hook'
require_relative '../lib/hooks_utils'
require_relative '../lib/gitlab_post_receive'
push_options = HooksUtils.get_push_options
if GitlabPostReceive.new(gl_repository, repo_path, key_id, refs, push_options).exec &&
GitlabCustomHook.new(repo_path, key_id).post_receive(refs)
exit 0
else
exit 1
end
#!/usr/bin/env ruby
#!/bin/sh
echo "The gitlab-shell hooks have been migrated to Gitaly, see https://gitlab.com/gitlab-org/gitaly/issues/1226"
exit 1
# This file was placed here by GitLab. It makes sure that your pushed commits
# will be processed properly.
refs = $stdin.read
key_id = ENV.delete('GL_ID')
protocol = ENV.delete('GL_PROTOCOL')
repo_path = Dir.pwd
gl_repository = ENV['GL_REPOSITORY']
def increase_reference_counter(gl_repository, repo_path)
result = GitlabNet.new.pre_receive(gl_repository)
result && result['reference_counter_increased']
end
require_relative '../lib/gitlab_custom_hook'
require_relative '../lib/gitlab_access'
require_relative '../lib/gitlab_net'
# It's important that on pre-receive `increase_reference_counter` gets executed
# last so that it only runs if everything else succeeded. On post-receive on the
# other hand, we run GitlabPostReceive first because the push is already done
# and we don't want to skip it if the custom hook fails.
if GitlabAccess.new(gl_repository, repo_path, key_id, refs, protocol).exec &&
GitlabCustomHook.new(repo_path, key_id).pre_receive(refs) &&
increase_reference_counter(gl_repository, repo_path)
exit 0
else
exit 1
end
#!/usr/bin/env ruby
#!/bin/sh
echo "The gitlab-shell hooks have been migrated to Gitaly, see https://gitlab.com/gitlab-org/gitaly/issues/1226"
exit 1
# This file was placed here by GitLab. It makes sure that your pushed commits
# will be processed properly.
ref_name = ARGV[0]
old_value = ARGV[1]
new_value = ARGV[2]
repo_path = Dir.pwd
key_id = ENV.delete('GL_ID')
require_relative '../lib/gitlab_custom_hook'
if GitlabCustomHook.new(repo_path, key_id).update(ref_name, old_value, new_value)
exit 0
else
exit 1
end
require_relative 'gitlab_init'
require_relative 'gitlab_net'
require_relative 'gitlab_access_status'
require_relative 'gitlab_metrics'
require_relative 'object_dirs_helper'
require 'json'
class GitlabAccess
class AccessDeniedError < StandardError; end
attr_reader :config, :gl_repository, :repo_path, :changes, :protocol
def initialize(gl_repository, repo_path, actor, changes, protocol)
@config = GitlabConfig.new
@gl_repository = gl_repository
@repo_path = repo_path.strip
@actor = actor
@changes = changes.lines
@protocol = protocol
end
def exec
status = GitlabMetrics.measure('check-access:git-receive-pack') do
api.check_access('git-receive-pack', @gl_repository, @repo_path, @actor, @changes, @protocol, env: ObjectDirsHelper.all_attributes.to_json)
end
raise AccessDeniedError, status.message unless status.allowed?
true
rescue GitlabNet::ApiUnreachableError
$stderr.puts "GitLab: Failed to authorize your Git request: internal API unreachable"
false
rescue AccessDeniedError => ex
$stderr.puts "GitLab: #{ex.message}"
false
end
protected
def api
GitlabNet.new
end
end
require 'open3'
require_relative 'gitlab_init'
require_relative 'gitlab_metrics'
class GitlabCustomHook
attr_reader :vars, :config
def initialize(repo_path, key_id)
@repo_path = repo_path
@vars = { 'GL_ID' => key_id }
@config = GitlabConfig.new
end
def pre_receive(changes)
GitlabMetrics.measure("pre-receive-hook") do
find_hooks('pre-receive').all? do |hook|
call_receive_hook(hook, changes)
end
end
end
def post_receive(changes)
GitlabMetrics.measure("post-receive-hook") do
find_hooks('post-receive').all? do |hook|
call_receive_hook(hook, changes)
end
end
end
def update(ref_name, old_value, new_value)
GitlabMetrics.measure("update-hook") do
find_hooks('update').all? do |hook|
system(vars, hook, ref_name, old_value, new_value)
end
end
end
private
def call_receive_hook(hook, changes)
# Prepare the hook subprocess. Attach a pipe to its stdin, and merge
# both its stdout and stderr into our own stdout.
stdin_reader, stdin_writer = IO.pipe
hook_pid = spawn(vars, hook, in: stdin_reader, err: :out)
stdin_reader.close
# Submit changes to the hook via its stdin.
begin
IO.copy_stream(StringIO.new(changes), stdin_writer)
rescue Errno::EPIPE # rubocop:disable Lint/HandleExceptions
# It is not an error if the hook does not consume all of its input.
end
# Close the pipe to let the hook know there is no further input.
stdin_writer.close
Process.wait(hook_pid)
$?.success?
end
# lookup hook files in this order:
#
# 1. <repository>.git/custom_hooks/<hook_name> - per project hook
# 2. <repository>.git/custom_hooks/<hook_name>.d/* - per project hooks
# 3. <repository>.git/hooks/<hook_name>.d/* - global hooks
#
def find_hooks(hook_name)
hook_files = []
# <repository>.git/custom_hooks/<hook_name>
project_custom_hook_file = File.join(@repo_path, 'custom_hooks', hook_name)
hook_files.push(project_custom_hook_file) if File.executable?(project_custom_hook_file)
# <repository>.git/custom_hooks/<hook_name>.d/*
project_custom_hooks_dir = File.join(@repo_path, 'custom_hooks', "#{hook_name}.d")
hook_files += match_hook_files(project_custom_hooks_dir)
# <repository>.git/hooks/<hook_name>.d/* OR <custom_hook_dir>/<hook_name>.d/*
global_custom_hooks_parent = config.custom_hooks_dir(default: File.join(@repo_path, 'hooks'))
global_custom_hooks_dir = File.join(global_custom_hooks_parent, "#{hook_name}.d")
hook_files += match_hook_files(global_custom_hooks_dir)
hook_files
end
# match files from path:
# 1. file must be executable
# 2. file must not match backup file
#
# the resulting list is sorted
def match_hook_files(path)
return [] unless Dir.exist?(path)
Dir["#{path}/*"].select do |f|
!f.end_with?('~') && File.executable?(f)
end.sort
end
end
......@@ -3,7 +3,6 @@ require 'openssl'
require 'json'
require_relative 'gitlab_config'
require_relative 'gitlab_access'
require_relative 'gitlab_lfs_authentication'
require_relative 'http_helper'
......
require_relative 'gitlab_init'
require_relative 'gitlab_net'
require_relative 'gitlab_metrics'
require 'json'
require 'base64'
require 'securerandom'
class GitlabPostReceive
attr_reader :config, :gl_repository, :repo_path, :changes, :jid
def initialize(gl_repository, repo_path, actor, changes, push_options)
@config = GitlabConfig.new
@gl_repository = gl_repository
@repo_path = repo_path.strip
@actor = actor
@changes = changes
@push_options = push_options
@jid = SecureRandom.hex(12)
end
def exec
response = GitlabMetrics.measure("post-receive") do
api.post_receive(gl_repository, @actor, changes, @push_options)
end
return false unless response
print_formatted_alert_message(response['broadcast_message']) if response['broadcast_message']
print_merge_request_links(response['merge_request_urls']) if response['merge_request_urls']
puts response['redirected_message'] if response['redirected_message']
puts response['project_created_message'] if response['project_created_message']
print_warnings(response['warnings']) if response['warnings']
response['reference_counter_decreased']
rescue GitlabNet::ApiUnreachableError
false
end
protected
def api
@api ||= GitlabNet.new
end
def print_merge_request_links(merge_request_urls)
return if merge_request_urls.empty?
puts
merge_request_urls.each { |mr| print_merge_request_link(mr) }
end
def print_merge_request_link(merge_request)
message =
if merge_request["new_merge_request"]
"To create a merge request for #{merge_request['branch_name']}, visit:"
else
"View merge request for #{merge_request['branch_name']}:"
end
puts message
puts((" " * 2) + merge_request["url"])
puts
end
def print_warnings(warnings)
message = "WARNINGS:\n#{warnings}"
print_formatted_alert_message(message)
end
def print_formatted_alert_message(message)
# A standard terminal window is (at least) 80 characters wide.
total_width = 80
# Git prefixes remote messages with "remote: ", so this width is subtracted
# from the width available to us.
total_width -= "remote: ".length # rubocop:disable Performance/FixedSize
# Our centered text shouldn't start or end right at the edge of the window,
# so we add some horizontal padding: 2 chars on either side.
text_width = total_width - 2 * 2
# Automatically wrap message at text_width (= 68) characters:
# Splits the message up into the longest possible chunks matching
# "<between 0 and text_width characters><space or end-of-line>".
msg_start_idx = 0
lines = []
while msg_start_idx < message.length
parsed_line = parse_broadcast_msg(message[msg_start_idx..-1], text_width)
msg_start_idx += parsed_line.length
lines.push(parsed_line.strip)
end
puts
puts "=" * total_width
puts
lines.each do |line|
line.strip!
# Center the line by calculating the left padding measured in characters.
line_padding = [(total_width - line.length) / 2, 0].max
puts((" " * line_padding) + line)
end
puts
puts "=" * total_width
end
private
def parse_broadcast_msg(msg, text_length)
msg ||= ""
# just return msg if shorter than or equal to text length
return msg if msg.length <= text_length
# search for word break shorter than text length
truncate_to_space = msg.match(/\A(.{,#{text_length}})(?=\s|$)(\s*)/).to_s
if truncate_to_space.empty?
# search for word break longer than text length
truncate_to_space = msg.match(/\A\S+/).to_s
end
truncate_to_space
end
end
require 'spec_helper'
require 'gitlab_access'
describe GitlabAccess do
let(:repository_path) { "/home/git/repositories" }
let(:repo_name) { 'dzaporozhets/gitlab-ci' }
let(:repo_path) { File.join(repository_path, repo_name) + ".git" }
let(:api) do
double(GitlabNet).tap do |api|
allow(api).to receive(:check_access).and_return(GitAccessStatus.new(true,
'200',
'ok',
gl_repository: 'project-1',
gl_project_path: 'group/subgroup/project',
gl_id: 'user-123',
gl_username: 'testuser',
git_config_options: ['receive.MaxInputSize=10000'],
gitaly: nil,
git_protocol: 'version=2'))
end
end
subject do
GitlabAccess.new(nil, repo_path, 'key-123', 'wow', 'ssh').tap do |access|
allow(access).to receive(:exec_cmd).and_return(:exec_called)
allow(access).to receive(:api).and_return(api)
end
end
before do
allow_any_instance_of(GitlabConfig).to receive(:repos_path).and_return(repository_path)
end
describe :initialize do
it { expect(subject.repo_path).to eq(repo_path) }
it { expect(subject.changes).to eq(['wow']) }
it { expect(subject.protocol).to eq('ssh') }
end
describe "#exec" do
context "access is granted" do
it "returns true" do
expect(subject.exec).to be_truthy
end
end
context "access is denied" do
before do
allow(api).to receive(:check_access).and_return(GitAccessStatus.new(
false,
'401',
'denied',
gl_repository: nil,
gl_project_path: nil,
gl_id: nil,
gl_username: nil,
git_config_options: nil,
gitaly: nil,
git_protocol: nil
))
end
it "returns false" do
expect(subject.exec).to be_falsey
end
end
context "API connection fails" do
before do
allow(api).to receive(:check_access).and_raise(GitlabNet::ApiUnreachableError)
end
it "returns false" do
expect(subject.exec).to be_falsey
end
end
end
end
This diff is collapsed.
This diff is collapsed.
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