diff --git a/Gemfile b/Gemfile index 8e181fee73b..fefccf87275 100644 --- a/Gemfile +++ b/Gemfile @@ -350,3 +350,6 @@ gem 'health_check', '~> 2.2.0' # System information gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' + +# Gitaly GRPC client +gem 'gitaly', '~> 0.2.1' diff --git a/Gemfile.lock b/Gemfile.lock index 4d6ce2a62b6..d5433f5d652 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -245,6 +245,9 @@ GEM json get_process_mem (0.2.0) gherkin-ruby (0.3.2) + gitaly (0.2.1) + google-protobuf (~> 3.1) + grpc (~> 1.0) github-linguist (4.7.6) charlock_holmes (~> 0.7.3) escape_utils (~> 1.1.0) @@ -296,6 +299,7 @@ GEM multi_json (~> 1.10) retriable (~> 1.4) signet (~> 0.6) + google-protobuf (3.2.0) googleauth (0.5.1) faraday (~> 0.9) jwt (~> 1.4) @@ -317,6 +321,9 @@ GEM grape-entity (0.6.0) activesupport multi_json (>= 1.3.2) + grpc (1.1.2) + google-protobuf (~> 3.1) + googleauth (~> 0.5.1) haml (4.0.7) tilt haml_lint (0.21.0) @@ -877,6 +884,7 @@ DEPENDENCIES fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) gemojione (~> 3.0) + gitaly (~> 0.2.1) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) diff --git a/changelogs/unreleased/gitaly-post-receive.yml b/changelogs/unreleased/gitaly-post-receive.yml new file mode 100644 index 00000000000..cf206e39084 --- /dev/null +++ b/changelogs/unreleased/gitaly-post-receive.yml @@ -0,0 +1,4 @@ +--- +title: Add internal API to notify Gitaly of post receive +merge_request: 8983 +author: diff --git a/config/initializers/8_gitaly.rb b/config/initializers/8_gitaly.rb new file mode 100644 index 00000000000..07dd30f0a24 --- /dev/null +++ b/config/initializers/8_gitaly.rb @@ -0,0 +1,2 @@ +# Make sure we initialize a Gitaly channel before Sidekiq starts multi-threaded execution. +Gitlab::GitalyClient.channel unless Rails.env.test? diff --git a/lib/api/internal.rb b/lib/api/internal.rb index d235977fbd8..7eed93aba00 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -132,6 +132,18 @@ module API { success: true, recovery_codes: codes } end + + post "/notify_post_receive" do + status 200 + + return unless Gitlab::GitalyClient.enabled? + + begin + Gitlab::GitalyClient::Notifications.new.post_receive(params[:repo_path]) + rescue GRPC::Unavailable => e + render_api_error(e, 500) + end + end end end end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb new file mode 100644 index 00000000000..b981a629fb0 --- /dev/null +++ b/lib/gitlab/gitaly_client.rb @@ -0,0 +1,29 @@ +require 'gitaly' + +module Gitlab + module GitalyClient + def self.gitaly_address + if Gitlab.config.gitaly.socket_path + "unix://#{Gitlab.config.gitaly.socket_path}" + end + end + + def self.channel + return @channel if defined?(@channel) + + @channel = + if enabled? + # NOTE: Gitaly currently runs on a Unix socket, so permissions are + # handled using the file system and no additional authentication is + # required (therefore the :this_channel_is_insecure flag) + GRPC::Core::Channel.new(gitaly_address, {}, :this_channel_is_insecure) + else + nil + end + end + + def self.enabled? + gitaly_address.present? + end + end +end diff --git a/lib/gitlab/gitaly_client/notifications.rb b/lib/gitlab/gitaly_client/notifications.rb new file mode 100644 index 00000000000..b827a56207f --- /dev/null +++ b/lib/gitlab/gitaly_client/notifications.rb @@ -0,0 +1,17 @@ +module Gitlab + module GitalyClient + class Notifications + attr_accessor :stub + + def initialize + @stub = Gitaly::Notifications::Stub.new(nil, nil, channel_override: GitalyClient.channel) + end + + def post_receive(repo_path) + repository = Gitaly::Repository.new(path: repo_path) + request = Gitaly::PostReceiveRequest.new(repository: repository) + stub.post_receive(request) + end + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/notifications_spec.rb b/spec/lib/gitlab/gitaly_client/notifications_spec.rb new file mode 100644 index 00000000000..a6252c99aa1 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/notifications_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::Notifications do + let(:client) { Gitlab::GitalyClient::Notifications.new } + + before do + allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket') + end + + describe '#post_receive' do + let(:repo_path) { '/path/to/my_repo.git' } + + it 'sends a post_receive message' do + expect_any_instance_of(Gitaly::Notifications::Stub). + to receive(:post_receive).with(post_receive_request_with_repo_path(repo_path)) + + client.post_receive(repo_path) + end + end +end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index ffeacb15f17..f18b8e98707 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -409,6 +409,34 @@ describe API::Internal, api: true do end end + describe 'POST /notify_post_receive' do + let(:valid_params) do + { repo_path: project.repository.path, secret_token: secret_token } + end + + before do + allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket') + end + + it "calls the Gitaly client if it's enabled" do + expect_any_instance_of(Gitlab::GitalyClient::Notifications). + to receive(:post_receive).with(project.repository.path) + + post api("/internal/notify_post_receive"), valid_params + + expect(response).to have_http_status(200) + end + + it "returns 500 if the gitaly call fails" do + expect_any_instance_of(Gitlab::GitalyClient::Notifications). + to receive(:post_receive).with(project.repository.path).and_raise(GRPC::Unavailable) + + post api("/internal/notify_post_receive"), valid_params + + expect(response).to have_http_status(500) + end + end + def project_with_repo_path(path) double().tap do |fake_project| allow(fake_project).to receive_message_chain('repository.path_to_repo' => path) diff --git a/spec/support/matchers/gitaly_matchers.rb b/spec/support/matchers/gitaly_matchers.rb new file mode 100644 index 00000000000..d7a53820684 --- /dev/null +++ b/spec/support/matchers/gitaly_matchers.rb @@ -0,0 +1,3 @@ +RSpec::Matchers.define :post_receive_request_with_repo_path do |path| + match { |actual| actual.repository.path == path } +end