From f8fa6e6f472179cfd9261a35d309d986f8db8ebd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com>
Date: Sun, 5 Feb 2017 15:04:23 -0300
Subject: [PATCH] Add internal endpoint to notify post-receive to Gitaly

---
 Gemfile                                       |  3 ++
 Gemfile.lock                                  |  8 +++++
 changelogs/unreleased/gitaly-post-receive.yml |  4 +++
 config/initializers/8_gitaly.rb               |  2 ++
 lib/api/internal.rb                           | 12 ++++++++
 lib/gitlab/gitaly_client.rb                   | 29 +++++++++++++++++++
 lib/gitlab/gitaly_client/notifications.rb     | 17 +++++++++++
 .../gitaly_client/notifications_spec.rb       | 20 +++++++++++++
 spec/requests/api/internal_spec.rb            | 28 ++++++++++++++++++
 spec/support/matchers/gitaly_matchers.rb      |  3 ++
 10 files changed, 126 insertions(+)
 create mode 100644 changelogs/unreleased/gitaly-post-receive.yml
 create mode 100644 config/initializers/8_gitaly.rb
 create mode 100644 lib/gitlab/gitaly_client.rb
 create mode 100644 lib/gitlab/gitaly_client/notifications.rb
 create mode 100644 spec/lib/gitlab/gitaly_client/notifications_spec.rb
 create mode 100644 spec/support/matchers/gitaly_matchers.rb

diff --git a/Gemfile b/Gemfile
index 8e181fee73bfc..fefccf87275b0 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 4d6ce2a62b62a..d5433f5d65229 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 0000000000000..cf206e39084bc
--- /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 0000000000000..07dd30f0a2489
--- /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 d235977fbd843..7eed93aba0060 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -132,6 +132,18 @@ class Internal < Grape::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 0000000000000..b981a629fb0a0
--- /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 0000000000000..b827a56207f6b
--- /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 0000000000000..a6252c99aa148
--- /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 ffeacb15f17a8..f18b8e9870781 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -409,6 +409,34 @@
     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 0000000000000..d7a538206845f
--- /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
-- 
GitLab