Commit c45341c8 authored by Alejandro Rodríguez's avatar Alejandro Rodríguez

Generate and handle a gl_repository param to pass around components

This new param allows us to share project information between components
that don't share or don't have access to the same filesystem
mountpoints, for example between Gitaly and Rails or between Rails and
Gitlab-Shell hooks. The previous parameters are still supported, but if
found, gl_repository is prefered. The old parameters should be deprecated
once all components support the new format.
parent c1e2da92
---
title: Generate and handle a gl_repository param to pass around components
merge_request: 10992
author:
module API module API
module Helpers module Helpers
module InternalHelpers module InternalHelpers
# Project paths may be any of the following:
# * /repository/storage/path/namespace/project
# * /namespace/project
# * namespace/project
#
# In addition, they may have a '.git' extension and multiple namespaces
#
# Transform all these cases to 'namespace/project'
def clean_project_path(project_path, storages = Gitlab.config.repositories.storages.values)
project_path = project_path.sub(/\.git\z/, '')
storages.each do |storage|
storage_path = File.expand_path(storage['path'])
if project_path.start_with?(storage_path)
project_path = project_path.sub(storage_path, '')
break
end
end
project_path.sub(/\A\//, '')
end
def project_path
@project_path ||= clean_project_path(params[:project])
end
def wiki? def wiki?
@wiki ||= project_path.end_with?('.wiki') && set_project unless defined?(@wiki)
!Project.find_by_full_path(project_path) @wiki
end end
def project def project
@project ||= begin set_project unless defined?(@project)
# Check for *.wiki repositories. @project
# Strip out the .wiki from the pathname before finding the
# project. This applies the correct project permissions to
# the wiki repository as well.
project_path.chomp!('.wiki') if wiki?
Project.find_by_full_path(project_path)
end
end end
def ssh_authentication_abilities def ssh_authentication_abilities
...@@ -66,6 +32,16 @@ module API ...@@ -66,6 +32,16 @@ module API
::Users::ActivityService.new(actor, 'Git SSH').execute if commands.include?(params[:action]) ::Users::ActivityService.new(actor, 'Git SSH').execute if commands.include?(params[:action])
end end
private
def set_project
if params[:gl_repository]
@project, @wiki = Gitlab::GlRepository.parse(params[:gl_repository])
else
@project, @wiki = Gitlab::RepoPath.parse(params[:project])
end
end
end end
end end
end end
...@@ -42,6 +42,10 @@ module API ...@@ -42,6 +42,10 @@ module API
if access_status.status if access_status.status
log_user_activity(actor) log_user_activity(actor)
# Project id to pass between components that don't share/don't have
# access to the same filesystem mounts
response[:gl_repository] = "#{wiki? ? 'wiki' : 'project'}-#{project.id}"
# Return the repository full path so that gitlab-shell has it when # Return the repository full path so that gitlab-shell has it when
# handling ssh commands # handling ssh commands
response[:repository_path] = response[:repository_path] =
...@@ -134,11 +138,9 @@ module API ...@@ -134,11 +138,9 @@ module API
return unless Gitlab::GitalyClient.enabled? return unless Gitlab::GitalyClient.enabled?
relative_path = Gitlab::RepoPath.strip_storage_path(params[:repo_path])
project = Project.find_by_full_path(relative_path.sub(/\.(git|wiki)\z/, ''))
begin begin
Gitlab::GitalyClient::Notifications.new(project.repository).post_receive repository = wiki? ? project.wiki.repository : project.repository
Gitlab::GitalyClient::Notifications.new(repository.raw_repository).post_receive
rescue GRPC::Unavailable => e rescue GRPC::Unavailable => e
render_api_error!(e, 500) render_api_error!(e, 500)
end end
......
module Gitlab
module GlRepository
def self.parse(gl_repository)
match_data = /\A(project|wiki)-([1-9][0-9]*)\z/.match(gl_repository)
unless match_data
raise ArgumentError, "Invalid GL Repository \"#{gl_repository}\""
end
type, id = match_data.captures
project = Project.find_by(id: id)
wiki = type == 'wiki'
[project, wiki]
end
end
end
...@@ -2,18 +2,29 @@ module Gitlab ...@@ -2,18 +2,29 @@ module Gitlab
module RepoPath module RepoPath
NotFoundError = Class.new(StandardError) NotFoundError = Class.new(StandardError)
def self.strip_storage_path(repo_path) def self.parse(repo_path)
result = nil project_path = strip_storage_path(repo_path.sub(/\.git\z/, ''), fail_on_not_found: false)
project = Project.find_by_full_path(project_path)
if project_path.end_with?('.wiki') && !project
project = Project.find_by_full_path(project_path.chomp('.wiki'))
wiki = true
else
wiki = false
end
Gitlab.config.repositories.storages.values.each do |params| [project, wiki]
storage_path = params['path']
if repo_path.start_with?(storage_path)
result = repo_path.sub(storage_path, '')
break
end end
def self.strip_storage_path(repo_path, fail_on_not_found: true)
result = repo_path
storage = Gitlab.config.repositories.storages.values.find do |params|
repo_path.start_with?(params['path'])
end end
if result.nil? if storage
result = result.sub(storage['path'], '')
elsif fail_on_not_found
raise NotFoundError.new("No known storage path matches #{repo_path.inspect}") raise NotFoundError.new("No known storage path matches #{repo_path.inspect}")
end end
......
require 'spec_helper'
describe ::Gitlab::GlRepository do
describe '.parse' do
set(:project) { create(:project) }
it 'parses a project gl_repository' do
expect(described_class.parse("project-#{project.id}")).to eq([project, false])
end
it 'parses a wiki gl_repository' do
expect(described_class.parse("wiki-#{project.id}")).to eq([project, true])
end
it 'throws an argument error on an invalid gl_repository' do
expect { described_class.parse("badformat-#{project.id}") }.to raise_error(ArgumentError)
end
end
end
require 'spec_helper' require 'spec_helper'
describe ::Gitlab::RepoPath do describe ::Gitlab::RepoPath do
describe '.parse' do
set(:project) { create(:project) }
it 'parses a full repository path' do
expect(described_class.parse(project.repository.path)).to eq([project, false])
end
it 'parses a full wiki path' do
expect(described_class.parse(project.wiki.repository.path)).to eq([project, true])
end
it 'parses a relative repository path' do
expect(described_class.parse(project.full_path + '.git')).to eq([project, false])
end
it 'parses a relative wiki path' do
expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true])
end
it 'parses a relative path starting with /' do
expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false])
end
end
describe '.strip_storage_path' do describe '.strip_storage_path' do
before do before do
allow(Gitlab.config.repositories).to receive(:storages).and_return({ allow(Gitlab.config.repositories).to receive(:storages).and_return({
......
require 'spec_helper'
describe ::API::Helpers::InternalHelpers do
include described_class
describe '.clean_project_path' do
project = 'namespace/project'
namespaced = File.join('namespace2', project)
{
File.join(Dir.pwd, project) => project,
File.join(Dir.pwd, namespaced) => namespaced,
project => project,
namespaced => namespaced,
project + '.git' => project,
namespaced + '.git' => namespaced,
"/" + project => project,
"/" + namespaced => namespaced,
}.each do |project_path, expected|
context project_path do
# Relative and absolute storage paths, with and without trailing /
['.', './', Dir.pwd, Dir.pwd + '/'].each do |storage_path|
context "storage path is #{storage_path}" do
subject { clean_project_path(project_path, [{ 'path' => storage_path }]) }
it { is_expected.to eq(expected) }
end
end
end
end
end
end
...@@ -180,6 +180,7 @@ describe API::Internal do ...@@ -180,6 +180,7 @@ describe API::Internal do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo) expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user).not_to have_an_activity_record expect(user).not_to have_an_activity_record
end end
end end
...@@ -191,6 +192,7 @@ describe API::Internal do ...@@ -191,6 +192,7 @@ describe API::Internal do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo) expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user).to have_an_activity_record expect(user).to have_an_activity_record
end end
end end
...@@ -202,6 +204,7 @@ describe API::Internal do ...@@ -202,6 +204,7 @@ describe API::Internal do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(user).to have_an_activity_record expect(user).to have_an_activity_record
end end
end end
...@@ -213,6 +216,7 @@ describe API::Internal do ...@@ -213,6 +216,7 @@ describe API::Internal do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(user).not_to have_an_activity_record expect(user).not_to have_an_activity_record
end end
...@@ -223,6 +227,7 @@ describe API::Internal do ...@@ -223,6 +227,7 @@ describe API::Internal do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
end end
end end
...@@ -233,6 +238,7 @@ describe API::Internal do ...@@ -233,6 +238,7 @@ describe API::Internal do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
end end
end end
end end
...@@ -444,18 +450,39 @@ describe API::Internal do ...@@ -444,18 +450,39 @@ describe API::Internal do
expect(json_response).to eq([]) expect(json_response).to eq([])
end end
context 'with a gl_repository parameter' do
let(:gl_repository) { "project-#{project.id}" }
it 'returns link to create new merge request' do
get api("/internal/merge_request_urls?gl_repository=#{gl_repository}&changes=#{changes}"), secret_token: secret_token
expect(json_response).to match [{
"branch_name" => "new_branch",
"url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
end
end end
describe 'POST /notify_post_receive' do describe 'POST /notify_post_receive' do
let(:valid_params) do let(:valid_params) do
{ repo_path: project.repository.path, secret_token: secret_token } { project: project.repository.path, secret_token: secret_token }
end
let(:valid_wiki_params) do
{ project: project.wiki.repository.path, secret_token: secret_token }
end end
before do before do
allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
end end
it "calls the Gitaly client if it's enabled" do it "calls the Gitaly client with the project's repository" do
expect(Gitlab::GitalyClient::Notifications).
to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)).
and_call_original
expect_any_instance_of(Gitlab::GitalyClient::Notifications). expect_any_instance_of(Gitlab::GitalyClient::Notifications).
to receive(:post_receive) to receive(:post_receive)
...@@ -464,6 +491,18 @@ describe API::Internal do ...@@ -464,6 +491,18 @@ describe API::Internal do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it "calls the Gitaly client with the wiki's repository if it's a wiki" do
expect(Gitlab::GitalyClient::Notifications).
to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)).
and_call_original
expect_any_instance_of(Gitlab::GitalyClient::Notifications).
to receive(:post_receive)
post api("/internal/notify_post_receive"), valid_wiki_params
expect(response).to have_http_status(200)
end
it "returns 500 if the gitaly call fails" do it "returns 500 if the gitaly call fails" do
expect_any_instance_of(Gitlab::GitalyClient::Notifications). expect_any_instance_of(Gitlab::GitalyClient::Notifications).
to receive(:post_receive).and_raise(GRPC::Unavailable) to receive(:post_receive).and_raise(GRPC::Unavailable)
...@@ -472,6 +511,40 @@ describe API::Internal do ...@@ -472,6 +511,40 @@ describe API::Internal do
expect(response).to have_http_status(500) expect(response).to have_http_status(500)
end end
context 'with a gl_repository parameter' do
let(:valid_params) do
{ gl_repository: "project-#{project.id}", secret_token: secret_token }
end
let(:valid_wiki_params) do
{ gl_repository: "wiki-#{project.id}", secret_token: secret_token }
end
it "calls the Gitaly client with the project's repository" do
expect(Gitlab::GitalyClient::Notifications).
to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)).
and_call_original
expect_any_instance_of(Gitlab::GitalyClient::Notifications).
to receive(:post_receive)
post api("/internal/notify_post_receive"), valid_params
expect(response).to have_http_status(200)
end
it "calls the Gitaly client with the wiki's repository if it's a wiki" do
expect(Gitlab::GitalyClient::Notifications).
to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)).
and_call_original
expect_any_instance_of(Gitlab::GitalyClient::Notifications).
to receive(:post_receive)
post api("/internal/notify_post_receive"), valid_wiki_params
expect(response).to have_http_status(200)
end
end
end end
def project_with_repo_path(path) def project_with_repo_path(path)
......
RSpec::Matchers.define :gitlab_git_repository_with do |values|
match do |actual|
actual.is_a?(Gitlab::Git::Repository) &&
values.all? { |k, v| actual.send(k) == v }
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