Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Thomas <nick@gitlab.com>2018-11-05 15:58:44 +0300
committerNick Thomas <nick@gitlab.com>2018-11-09 18:27:41 +0300
commitef3679992eaa56a85cde6c9fd50ec41cf733f395 (patch)
treea916e68050ec0755286ec9f9903379dbc8c4e9e9
parent1a4581d04d67d2821e5df6866af8ba6bbef40960 (diff)
Support SSH credentials for push mirroring
-rw-r--r--changelogs/unreleased/ssh-credentials-for-remote-mirroring.yml5
-rw-r--r--ruby/lib/gitaly_server/remote_service.rb10
-rw-r--r--ruby/lib/gitaly_server/repository_service.rb14
-rw-r--r--ruby/lib/gitlab/git/gitlab_projects.rb72
-rw-r--r--ruby/lib/gitlab/git/remote_mirror.rb67
-rw-r--r--ruby/lib/gitlab/git/repository.rb13
-rw-r--r--ruby/lib/gitlab/git/repository_mirroring.rb26
-rw-r--r--ruby/lib/gitlab/git/ssh_auth.rb73
-rw-r--r--ruby/spec/gitaly/remote_service_spec.rb45
-rw-r--r--ruby/spec/gitaly/repository_service_spec.rb45
-rw-r--r--ruby/spec/lib/gitlab/git/gitlab_projects_spec.rb91
-rw-r--r--ruby/spec/lib/gitlab/git/remote_mirror_spec.rb67
-rw-r--r--ruby/spec/lib/gitlab/git/ssh_auth_spec.rb97
-rw-r--r--ruby/spec/test_repo_helper.rb15
14 files changed, 462 insertions, 178 deletions
diff --git a/changelogs/unreleased/ssh-credentials-for-remote-mirroring.yml b/changelogs/unreleased/ssh-credentials-for-remote-mirroring.yml
new file mode 100644
index 000000000..3841b1b80
--- /dev/null
+++ b/changelogs/unreleased/ssh-credentials-for-remote-mirroring.yml
@@ -0,0 +1,5 @@
+---
+title: Support SSH credentials for push mirroring
+merge_request: 959
+author:
+type: added
diff --git a/ruby/lib/gitaly_server/remote_service.rb b/ruby/lib/gitaly_server/remote_service.rb
index f5aac8ed3..6dca47d3d 100644
--- a/ruby/lib/gitaly_server/remote_service.rb
+++ b/ruby/lib/gitaly_server/remote_service.rb
@@ -44,8 +44,14 @@ module GitalyServer
only_branches_matching += request_enum.flat_map(&:only_branches_matching)
- remote_mirror = Gitlab::Git::RemoteMirror.new(repo, first_request.ref_name)
- remote_mirror.update(only_branches_matching: only_branches_matching)
+ remote_mirror = Gitlab::Git::RemoteMirror.new(
+ repo,
+ first_request.ref_name,
+ only_branches_matching: only_branches_matching,
+ ssh_auth: Gitlab::Git::SshAuth.from_gitaly(first_request)
+ )
+
+ remote_mirror.update
Gitaly::UpdateRemoteMirrorResponse.new
end
diff --git a/ruby/lib/gitaly_server/repository_service.rb b/ruby/lib/gitaly_server/repository_service.rb
index 603a076bb..6b34be69c 100644
--- a/ruby/lib/gitaly_server/repository_service.rb
+++ b/ruby/lib/gitaly_server/repository_service.rb
@@ -28,11 +28,15 @@ module GitalyServer
bridge_exceptions do
gitlab_projects = Gitlab::Git::GitlabProjects.from_gitaly(request.repository, call)
- success = gitlab_projects.fetch_remote(request.remote, request.timeout,
- force: request.force,
- tags: !request.no_tags,
- ssh_key: request.ssh_key.presence,
- known_hosts: request.known_hosts.presence)
+ success = Gitlab::Git::SshAuth.from_gitaly(request).setup do |env|
+ gitlab_projects.fetch_remote(
+ request.remote,
+ request.timeout,
+ force: request.force,
+ tags: !request.no_tags,
+ env: env
+ )
+ end
raise GRPC::Unknown.new("Fetching remote #{request.remote} failed: #{gitlab_projects.output}") unless success
diff --git a/ruby/lib/gitlab/git/gitlab_projects.rb b/ruby/lib/gitlab/git/gitlab_projects.rb
index 6ad7e1478..011c01f06 100644
--- a/ruby/lib/gitlab/git/gitlab_projects.rb
+++ b/ruby/lib/gitlab/git/gitlab_projects.rb
@@ -61,37 +61,35 @@ module Gitlab
end
end
- def fetch_remote(name, timeout, force:, tags:, ssh_key: nil, known_hosts: nil, prune: true)
+ def fetch_remote(name, timeout, force:, tags:, env: {}, prune: true)
logger.info "Fetching remote #{name} for repository #{repository_absolute_path}."
cmd = fetch_remote_command(name, tags, prune, force)
- setup_ssh_auth(ssh_key, known_hosts) do |env|
- run_with_timeout(cmd, timeout, repository_absolute_path, env).tap do |success|
- logger.error "Fetching remote #{name} for repository #{repository_absolute_path} failed." unless success
- end
+ run_with_timeout(cmd, timeout, repository_absolute_path, env).tap do |success|
+ logger.error "Fetching remote #{name} for repository #{repository_absolute_path} failed." unless success
end
end
- def push_branches(remote_name, timeout, force, branch_names)
+ def push_branches(remote_name, timeout, force, branch_names, env: {})
logger.info "Pushing branches from #{repository_absolute_path} to remote #{remote_name}: #{branch_names}"
cmd = %W(#{Gitlab.config.git.bin_path} push)
cmd << '--force' if force
cmd += %W(-- #{remote_name}).concat(branch_names)
- success = run_with_timeout(cmd, timeout, repository_absolute_path)
+ success = run_with_timeout(cmd, timeout, repository_absolute_path, env)
logger.error("Pushing branches to remote #{remote_name} failed.") unless success
success
end
- def delete_remote_branches(remote_name, branch_names)
+ def delete_remote_branches(remote_name, branch_names, env: {})
branches = branch_names.map { |branch_name| ":#{branch_name}" }
logger.info "Pushing deleted branches from #{repository_absolute_path} to remote #{remote_name}: #{branch_names}"
cmd = %W(#{Gitlab.config.git.bin_path} push -- #{remote_name}).concat(branches)
- success = run(cmd, repository_absolute_path)
+ success = run(cmd, repository_absolute_path, env)
logger.error("Pushing deleted branches to remote #{remote_name} failed.") unless success
@@ -132,62 +130,6 @@ module Gitlab
run(cmd, repository_absolute_path)
end
- # Builds a small shell script that can be used to execute SSH with a set of
- # custom options.
- #
- # Options are expanded as `'-oKey="Value"'`, so SSH will correctly interpret
- # paths with spaces in them. We trust the user not to embed single or double
- # quotes in the key or value.
- def custom_ssh_script(options = {})
- args = options.map { |k, v| %{'-o#{k}="#{v}"'} }.join(' ')
-
- [
- "#!/bin/sh",
- "exec ssh #{args} \"$@\""
- ].join("\n")
- end
-
- # Known hosts data and private keys can be passed to gitlab-shell in the
- # environment. If present, this method puts them into temporary files, writes
- # a script that can substitute as `ssh`, setting the options to respect those
- # files, and yields: { "GIT_SSH" => "/tmp/myScript" }
- def setup_ssh_auth(key, known_hosts)
- options = {}
-
- if key
- key_file = Tempfile.new('gitlab-shell-key-file')
- key_file.chmod(0o400)
- key_file.write(key)
- key_file.close
-
- options['IdentityFile'] = key_file.path
- options['IdentitiesOnly'] = 'yes'
- end
-
- if known_hosts
- known_hosts_file = Tempfile.new('gitlab-shell-known-hosts')
- known_hosts_file.chmod(0o400)
- known_hosts_file.write(known_hosts)
- known_hosts_file.close
-
- options['StrictHostKeyChecking'] = 'yes'
- options['UserKnownHostsFile'] = known_hosts_file.path
- end
-
- return yield({}) if options.empty?
-
- script = Tempfile.new('gitlab-shell-ssh-wrapper')
- script.chmod(0o755)
- script.write(custom_ssh_script(options))
- script.close
-
- yield('GIT_SSH' => script.path)
- ensure
- key_file&.close!
- known_hosts_file&.close!
- script&.close!
- end
-
private
def fetch_remote_command(name, tags, prune, force)
diff --git a/ruby/lib/gitlab/git/remote_mirror.rb b/ruby/lib/gitlab/git/remote_mirror.rb
index edfa9fe6a..9d520aca9 100644
--- a/ruby/lib/gitlab/git/remote_mirror.rb
+++ b/ruby/lib/gitlab/git/remote_mirror.rb
@@ -1,30 +1,45 @@
module Gitlab
module Git
class RemoteMirror
- def initialize(repository, ref_name)
+ attr_reader :repository, :ref_name, :ssh_auth, :only_branches_matching
+
+ def initialize(repository, ref_name, ssh_auth:, only_branches_matching: [])
@repository = repository
@ref_name = ref_name
+ @ssh_auth = ssh_auth
+ @only_branches_matching = only_branches_matching
end
- def update(only_branches_matching: [])
- local_branches = refs_obj(@repository.local_branches, only_refs_matching: only_branches_matching)
- remote_branches = refs_obj(@repository.remote_branches(@ref_name), only_refs_matching: only_branches_matching)
-
- updated_branches = changed_refs(local_branches, remote_branches)
- push_branches(updated_branches.keys) if updated_branches.present?
+ def update
+ ssh_auth.setup do |env|
+ updated_branches = changed_refs(local_branches, remote_branches)
+ push_refs(default_branch_first(updated_branches.keys), env: env)
+ delete_refs(local_branches, remote_branches, env: env)
- delete_refs(local_branches, remote_branches)
+ local_tags = refs_obj(repository.tags)
+ remote_tags = refs_obj(repository.remote_tags(ref_name, env: env))
- local_tags = refs_obj(@repository.tags)
- remote_tags = refs_obj(@repository.remote_tags(@ref_name))
+ updated_tags = changed_refs(local_tags, remote_tags)
+ push_refs(updated_tags.keys, env: env)
+ delete_refs(local_tags, remote_tags, env: env)
+ end
+ end
- updated_tags = changed_refs(local_tags, remote_tags)
- @repository.push_remote_branches(@ref_name, updated_tags.keys) if updated_tags.present?
+ private
- delete_refs(local_tags, remote_tags)
+ def local_branches
+ @local_branches ||= refs_obj(
+ repository.local_branches,
+ only_refs_matching: only_branches_matching
+ )
end
- private
+ def remote_branches
+ @remote_branches ||= refs_obj(
+ repository.remote_branches(ref_name),
+ only_refs_matching: only_branches_matching
+ )
+ end
def refs_obj(refs, only_refs_matching: [])
refs.each_with_object({}) do |ref, refs|
@@ -42,32 +57,40 @@ module Gitlab
end
end
- def push_branches(branches)
+ # Put the default branch first so it works fine when remote mirror is empty.
+ def default_branch_first(branches)
+ return unless branches.present?
+
default_branch, branches = branches.partition do |branch|
- @repository.root_ref == branch
+ repository.root_ref == branch
end
- # Push the default branch first so it works fine when remote mirror is empty.
branches.unshift(*default_branch)
+ end
- @repository.push_remote_branches(@ref_name, branches)
+ def push_refs(refs, env:)
+ return unless refs.present?
+
+ repository.push_remote_branches(ref_name, refs, env: env)
end
- def delete_refs(local_refs, remote_refs)
+ def delete_refs(local_refs, remote_refs, env:)
refs = refs_to_delete(local_refs, remote_refs)
- @repository.delete_remote_branches(@ref_name, refs.keys) if refs.present?
+ return unless refs.present?
+
+ repository.delete_remote_branches(ref_name, refs.keys, env: env)
end
def refs_to_delete(local_refs, remote_refs)
- default_branch_id = @repository.commit.id
+ default_branch_id = repository.commit.id
remote_refs.select do |remote_ref_name, remote_ref|
next false if local_refs[remote_ref_name] # skip if branch or tag exist in local repo
remote_ref_id = remote_ref.dereferenced_target.try(:id)
- remote_ref_id && @repository.ancestor?(remote_ref_id, default_branch_id)
+ remote_ref_id && repository.ancestor?(remote_ref_id, default_branch_id)
end
end
end
diff --git a/ruby/lib/gitlab/git/repository.rb b/ruby/lib/gitlab/git/repository.rb
index 97779f7e2..8d2b8bcbd 100644
--- a/ruby/lib/gitlab/git/repository.rb
+++ b/ruby/lib/gitlab/git/repository.rb
@@ -24,7 +24,6 @@ module Gitlab
SQUASH_WORKTREE_PREFIX = 'squash'.freeze
AM_WORKTREE_PREFIX = 'am'.freeze
GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze
- GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout
AUTOCRLF_VALUES = { 'true' => true, 'false' => false, 'input' => :input }.freeze
RUGGED_KEY = :rugged_list
@@ -537,18 +536,6 @@ module Gitlab
end
end
- def push_remote_branches(remote_name, branch_names, forced: true)
- success = @gitlab_projects.push_branches(remote_name, GITLAB_PROJECTS_TIMEOUT, forced, branch_names)
-
- success || gitlab_projects_error
- end
-
- def delete_remote_branches(remote_name, branch_names)
- success = @gitlab_projects.delete_remote_branches(remote_name, branch_names)
-
- success || gitlab_projects_error
- end
-
def multi_action(
user, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
diff --git a/ruby/lib/gitlab/git/repository_mirroring.rb b/ruby/lib/gitlab/git/repository_mirroring.rb
index d3750ec53..26534051d 100644
--- a/ruby/lib/gitlab/git/repository_mirroring.rb
+++ b/ruby/lib/gitlab/git/repository_mirroring.rb
@@ -1,6 +1,10 @@
module Gitlab
module Git
module RepositoryMirroring
+ GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout
+
+ RemoteError = Class.new(StandardError)
+
REFMAPS = {
# With `:all_refs`, the repository is equivalent to the result of `git clone --mirror`
all_refs: '+refs/*:refs/*',
@@ -25,6 +29,18 @@ module Gitlab
branches
end
+ def push_remote_branches(remote_name, branch_names, forced: true, env: {})
+ success = @gitlab_projects.push_branches(remote_name, GITLAB_PROJECTS_TIMEOUT, forced, branch_names, env: env)
+
+ success || gitlab_projects_error
+ end
+
+ def delete_remote_branches(remote_name, branch_names, env: {})
+ success = @gitlab_projects.delete_remote_branches(remote_name, branch_names, env: env)
+
+ success || gitlab_projects_error
+ end
+
def set_remote_as_mirror(remote_name, refmap: :all_refs)
set_remote_refmap(remote_name, refmap)
@@ -32,15 +48,15 @@ module Gitlab
rugged.config["remote.#{remote_name}.prune"] = true
end
- def remote_tags(remote)
+ def remote_tags(remote, env: {})
# Each line has this format: "dc872e9fa6963f8f03da6c8f6f264d0845d6b092\trefs/tags/v1.10.0\n"
# We want to convert it to: [{ 'v1.10.0' => 'dc872e9fa6963f8f03da6c8f6f264d0845d6b092' }, ...]
- list_remote_tags(remote).map do |line|
+ list_remote_tags(remote, env: env).map do |line|
target, path = line.strip.split("\t")
# When the remote repo does not have tags.
if target.nil? || path.nil?
- Rails.logger.info "Empty or invalid list of tags for remote: #{remote}. Output: #{output}"
+ Rails.logger.info "Empty or invalid list of tags for remote: #{remote}"
break []
end
@@ -73,11 +89,11 @@ module Gitlab
end
end
- def list_remote_tags(remote)
+ def list_remote_tags(remote, env:)
tag_list, exit_code, error = nil
cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-remote --tags #{remote}]
- Open3.popen3(*cmd) do |_stdin, stdout, stderr, wait_thr|
+ Open3.popen3(env, *cmd) do |_stdin, stdout, stderr, wait_thr|
tag_list = stdout.read
error = stderr.read
exit_code = wait_thr.value.exitstatus
diff --git a/ruby/lib/gitlab/git/ssh_auth.rb b/ruby/lib/gitlab/git/ssh_auth.rb
new file mode 100644
index 000000000..80cb6401c
--- /dev/null
+++ b/ruby/lib/gitlab/git/ssh_auth.rb
@@ -0,0 +1,73 @@
+module Gitlab
+ module Git
+ # SshAuth writes custom identity and known_hosts files to temporary files
+ # and builds a `GIT_SSH_COMMAND` environment variable to allow git
+ # operations over SSH to take advantage of them.
+ #
+ # To use:
+ # SshAuth.from_gitaly(request).setup do |env|
+ # # Run commands here with the provided environment
+ # end
+ class SshAuth
+ attr_reader :ssh_key, :known_hosts
+
+ def self.from_gitaly(request)
+ new(request.ssh_key, request.known_hosts)
+ end
+
+ def initialize(ssh_key, known_hosts)
+ @ssh_key = ssh_key
+ @known_hosts = known_hosts
+ end
+
+ def setup
+ options = {}
+
+ if ssh_key.present?
+ key_file = write_tempfile('gitlab-shell-key-file', 0o400, ssh_key)
+
+ options['IdentityFile'] = key_file.path
+ options['IdentitiesOnly'] = 'yes'
+ end
+
+ if known_hosts.present?
+ known_hosts_file = write_tempfile('gitlab-shell-known-hosts', 0o400, known_hosts)
+
+ options['StrictHostKeyChecking'] = 'yes'
+ options['UserKnownHostsFile'] = known_hosts_file.path
+ end
+
+ yield custom_environment(options)
+ ensure
+ key_file&.close!
+ known_hosts_file&.close!
+ end
+
+ private
+
+ def write_tempfile(name, mode, data)
+ Tempfile.open(name) do |tempfile|
+ tempfile.chmod(mode)
+ tempfile.write(data)
+
+ # Return the tempfile instance so it can be unlinked
+ tempfile
+ end
+ end
+
+ # Constructs an environment that will make SSH, as invoked by git, respect
+ # the given options. To achieve this, we use the GIT_SSH_COMMAND envvar.
+ #
+ # Options are expanded as `'-oKey="Value"'`, so SSH will correctly
+ # interpret paths with spaces in them. We trust the rest of this file not
+ # to embed single or double quotes in the key or value.
+ def custom_environment(options)
+ return {} unless options.present?
+
+ args = options.map { |k, v| %('-o#{k}="#{v}"') }
+
+ { 'GIT_SSH_COMMAND' => %(ssh #{args.join(' ')}) }
+ end
+ end
+ end
+end
diff --git a/ruby/spec/gitaly/remote_service_spec.rb b/ruby/spec/gitaly/remote_service_spec.rb
new file mode 100644
index 000000000..5dac8a6b4
--- /dev/null
+++ b/ruby/spec/gitaly/remote_service_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitaly::RemoteService do
+ include IntegrationClient
+ include TestRepo
+
+ subject { gitaly_stub(:RemoteService) }
+
+ describe 'UpdateRemoteMirror' do
+ let(:call) { double(metadata: { 'gitaly-storage-path' => '/path/to/storage' }) }
+ let(:repo) { gitaly_repo('default', 'foobar.git') }
+ let(:remote) { 'my-remote' }
+
+ context 'request does not have ssh_key and known_hosts set' do
+ it 'performs the mirroring update with an empty environment' do
+ request = Gitaly::UpdateRemoteMirrorRequest.new(repository: repo, ref_name: remote)
+
+ allow(call).to receive(:each_remote_read).and_return(double(next: request, flat_map: []))
+ allow(Gitlab::Git::Repository).to receive(:from_gitaly).and_return(repo)
+ allow_any_instance_of(Gitlab::Git::RemoteMirror).to receive(:update)
+ expect(Gitlab::Git::SshAuth).to receive(:new).with('', '')
+
+ GitalyServer::RemoteService.new.update_remote_mirror(call)
+ end
+ end
+
+ context 'request has ssh_key and known_hosts set' do
+ it 'calls GitlabProjects#fetch_remote with a custom GIT_SSH_COMMAND' do
+ request = Gitaly::UpdateRemoteMirrorRequest.new(
+ repository: repo,
+ ref_name: remote,
+ ssh_key: 'SSH KEY',
+ known_hosts: 'KNOWN HOSTS'
+ )
+
+ allow(call).to receive(:each_remote_read).and_return(double(next: request, flat_map: []))
+ allow(Gitlab::Git::Repository).to receive(:from_gitaly).and_return(repo)
+ allow_any_instance_of(Gitlab::Git::RemoteMirror).to receive(:update)
+ expect(Gitlab::Git::SshAuth).to receive(:new).with('SSH KEY', 'KNOWN HOSTS')
+
+ GitalyServer::RemoteService.new.update_remote_mirror(call)
+ end
+ end
+ end
+end
diff --git a/ruby/spec/gitaly/repository_service_spec.rb b/ruby/spec/gitaly/repository_service_spec.rb
index d18f18216..e4c9bcf95 100644
--- a/ruby/spec/gitaly/repository_service_spec.rb
+++ b/ruby/spec/gitaly/repository_service_spec.rb
@@ -21,20 +21,39 @@ describe Gitaly::RepositoryService do
end
describe 'FetchRemote' do
+ let(:call) { double(metadata: { 'gitaly-storage-path' => '/path/to/storage' }) }
+ let(:repo) { gitaly_repo('default', 'foobar.git') }
+ let(:remote) { 'my-remote' }
+
+ let(:gl_projects) { double('Gitlab::Git::GitlabProjects') }
+
+ before do
+ allow(Gitlab::Git::GitlabProjects).to receive(:from_gitaly).and_return(gl_projects)
+ end
+
context 'request does not have ssh_key and known_hosts set' do
- it 'calls GitlabProjects#fetch_remote with nil ssh_key and known_hosts' do
- call = double(metadata: { 'gitaly-storage-path' => '/path/to/storage' })
- request = Gitaly::FetchRemoteRequest.new(repository: gitaly_repo('default', 'foobar.git'), remote: 'my-remote')
-
- gl_projects_double = double('Gitlab::Git::GitlabProjects')
- allow(Gitlab::Git::GitlabProjects).to receive(:from_gitaly).and_return(gl_projects_double)
-
- expect(gl_projects_double).to receive(:fetch_remote)
- .with('my-remote', 0,
- force: false,
- tags: true,
- ssh_key: nil,
- known_hosts: nil)
+ it 'calls GitlabProjects#fetch_remote with an empty environment' do
+ request = Gitaly::FetchRemoteRequest.new(repository: repo, remote: remote)
+
+ expect(gl_projects).to receive(:fetch_remote)
+ .with(remote, 0, force: false, tags: true, env: {})
+ .and_return(true)
+
+ GitalyServer::RepositoryService.new.fetch_remote(request, call)
+ end
+ end
+
+ context 'request has ssh_key and known_hosts set' do
+ it 'calls GitlabProjects#fetch_remote with a custom GIT_SSH_COMMAND' do
+ request = Gitaly::FetchRemoteRequest.new(
+ repository: repo,
+ remote: remote,
+ ssh_key: 'SSH KEY',
+ known_hosts: 'KNOWN HOSTS'
+ )
+
+ expect(gl_projects).to receive(:fetch_remote)
+ .with(remote, 0, force: false, tags: true, env: { 'GIT_SSH_COMMAND' => /ssh/ })
.and_return(true)
GitalyServer::RepositoryService.new.fetch_remote(request, call)
diff --git a/ruby/spec/lib/gitlab/git/gitlab_projects_spec.rb b/ruby/spec/lib/gitlab/git/gitlab_projects_spec.rb
index b85fd6867..391a4b76a 100644
--- a/ruby/spec/lib/gitlab/git/gitlab_projects_spec.rb
+++ b/ruby/spec/lib/gitlab/git/gitlab_projects_spec.rb
@@ -26,8 +26,20 @@ describe Gitlab::Git::GitlabProjects do
def stub_spawn(*args, success: true)
exitstatus = success ? 0 : nil
- expect(gl_projects).to receive(:popen_with_timeout).with(*args)
- .and_return(["output", exitstatus])
+
+ expect(gl_projects)
+ .to receive(:popen_with_timeout)
+ .with(*args)
+ .and_return(["output", exitstatus])
+ end
+
+ def stub_unlimited_spawn(*args, success: true)
+ exitstatus = success ? 0 : nil
+
+ expect(gl_projects)
+ .to receive(:popen)
+ .with(*args)
+ .and_return(["output", exitstatus])
end
def stub_spawn_timeout(*args)
@@ -45,19 +57,20 @@ describe Gitlab::Git::GitlabProjects do
describe '#push_branches' do
let(:remote_name) { 'remote-name' }
let(:branch_name) { 'master' }
+ let(:env) { { 'GIT_SSH_COMMAND' => 'foo-command bar' } }
let(:cmd) { %W(#{Gitlab.config.git.bin_path} push -- #{remote_name} #{branch_name}) }
let(:force) { false }
- subject { gl_projects.push_branches(remote_name, 600, force, [branch_name]) }
+ subject { gl_projects.push_branches(remote_name, 600, force, [branch_name], env: env) }
it 'executes the command' do
- stub_spawn(cmd, 600, tmp_repo_path, success: true)
+ stub_spawn(cmd, 600, tmp_repo_path, env, success: true)
is_expected.to be_truthy
end
it 'fails' do
- stub_spawn(cmd, 600, tmp_repo_path, success: false)
+ stub_spawn(cmd, 600, tmp_repo_path, env, success: false)
is_expected.to be_falsy
end
@@ -67,7 +80,7 @@ describe Gitlab::Git::GitlabProjects do
let(:force) { true }
it 'executes the command' do
- stub_spawn(cmd, 600, tmp_repo_path, success: true)
+ stub_spawn(cmd, 600, tmp_repo_path, env, success: true)
is_expected.to be_truthy
end
@@ -78,36 +91,23 @@ describe Gitlab::Git::GitlabProjects do
let(:remote_name) { 'remote-name' }
let(:branch_name) { 'master' }
let(:force) { false }
- let(:prune) { true }
let(:tags) { true }
- let(:args) { { force: force, tags: tags, prune: prune }.merge(extra_args) }
- let(:extra_args) { {} }
+ let(:env) { { 'GIT_SSH_COMMAND' => 'foo-command bar' } }
+ let(:prune) { true }
+ let(:args) { { force: force, tags: tags, env: env, prune: prune } }
let(:cmd) { %W(#{Gitlab.config.git.bin_path} fetch #{remote_name} --quiet --prune --tags) }
subject { gl_projects.fetch_remote(remote_name, 600, args) }
- def stub_tempfile(name, filename, opts = {})
- chmod = opts.delete(:chmod)
- file = StringIO.new
-
- allow(file).to receive(:close!)
- allow(file).to receive(:path).and_return(name)
-
- expect(Tempfile).to receive(:new).with(filename).and_return(file)
- expect(file).to receive(:chmod).with(chmod) if chmod
-
- file
- end
-
context 'with default args' do
it 'executes the command' do
- stub_spawn(cmd, 600, tmp_repo_path, {}, success: true)
+ stub_spawn(cmd, 600, tmp_repo_path, env, success: true)
is_expected.to be_truthy
end
it 'returns false if the command fails' do
- stub_spawn(cmd, 600, tmp_repo_path, {}, success: false)
+ stub_spawn(cmd, 600, tmp_repo_path, env, success: false)
is_expected.to be_falsy
end
@@ -118,7 +118,7 @@ describe Gitlab::Git::GitlabProjects do
let(:cmd) { %W(#{Gitlab.config.git.bin_path} fetch #{remote_name} --quiet --prune --force --tags) }
it 'executes the command with forced option' do
- stub_spawn(cmd, 600, tmp_repo_path, {}, success: true)
+ stub_spawn(cmd, 600, tmp_repo_path, env, success: true)
is_expected.to be_truthy
end
@@ -129,7 +129,7 @@ describe Gitlab::Git::GitlabProjects do
let(:cmd) { %W(#{Gitlab.config.git.bin_path} fetch #{remote_name} --quiet --prune --no-tags) }
it 'executes the command' do
- stub_spawn(cmd, 600, tmp_repo_path, {}, success: true)
+ stub_spawn(cmd, 600, tmp_repo_path, env, success: true)
is_expected.to be_truthy
end
@@ -140,42 +140,31 @@ describe Gitlab::Git::GitlabProjects do
let(:cmd) { %W(#{Gitlab.config.git.bin_path} fetch #{remote_name} --quiet --tags) }
it 'executes the command' do
- stub_spawn(cmd, 600, tmp_repo_path, {}, success: true)
+ stub_spawn(cmd, 600, tmp_repo_path, env, success: true)
is_expected.to be_truthy
end
end
+ end
- describe 'with an SSH key' do
- let(:extra_args) { { ssh_key: 'SSH KEY' } }
-
- it 'sets GIT_SSH to a custom script' do
- script = stub_tempfile('scriptFile', 'gitlab-shell-ssh-wrapper', chmod: 0o755)
- key = stub_tempfile('/tmp files/keyFile', 'gitlab-shell-key-file', chmod: 0o400)
+ describe '#delete_remote_branches' do
+ let(:remote_name) { 'remote-name' }
+ let(:branch_name) { 'master' }
+ let(:env) { { 'GIT_SSH_COMMAND' => 'foo-command bar' } }
+ let(:cmd) { %W(#{Gitlab.config.git.bin_path} push -- #{remote_name} :#{branch_name}) }
- stub_spawn(cmd, 600, tmp_repo_path, { 'GIT_SSH' => 'scriptFile' }, success: true)
+ subject { gl_projects.delete_remote_branches(remote_name, [branch_name], env: env) }
- is_expected.to be_truthy
+ it 'executes the command' do
+ stub_unlimited_spawn(cmd, tmp_repo_path, env, success: true)
- expect(script.string).to eq("#!/bin/sh\nexec ssh '-oIdentityFile=\"/tmp files/keyFile\"' '-oIdentitiesOnly=\"yes\"' \"$@\"")
- expect(key.string).to eq('SSH KEY')
- end
+ is_expected.to be_truthy
end
- describe 'with known_hosts data' do
- let(:extra_args) { { known_hosts: 'KNOWN HOSTS' } }
-
- it 'sets GIT_SSH to a custom script' do
- script = stub_tempfile('scriptFile', 'gitlab-shell-ssh-wrapper', chmod: 0o755)
- key = stub_tempfile('/tmp files/knownHosts', 'gitlab-shell-known-hosts', chmod: 0o400)
-
- stub_spawn(cmd, 600, tmp_repo_path, { 'GIT_SSH' => 'scriptFile' }, success: true)
-
- is_expected.to be_truthy
+ it 'fails' do
+ stub_unlimited_spawn(cmd, tmp_repo_path, env, success: false)
- expect(script.string).to eq("#!/bin/sh\nexec ssh '-oStrictHostKeyChecking=\"yes\"' '-oUserKnownHostsFile=\"/tmp files/knownHosts\"' \"$@\"")
- expect(key.string).to eq('KNOWN HOSTS')
- end
+ is_expected.to be_falsy
end
end
end
diff --git a/ruby/spec/lib/gitlab/git/remote_mirror_spec.rb b/ruby/spec/lib/gitlab/git/remote_mirror_spec.rb
new file mode 100644
index 000000000..a009a2f92
--- /dev/null
+++ b/ruby/spec/lib/gitlab/git/remote_mirror_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Gitlab::Git::RemoteMirror do
+ include TestRepo
+
+ let(:repository) { gitlab_git_from_gitaly_with_gitlab_projects(new_mutable_test_repo) }
+ let(:gitlab_projects) { repository.gitlab_projects }
+ let(:ref_name) { 'remote' }
+ let(:ssh_key) { 'SSH KEY' }
+ let(:known_hosts) { 'KNOWN HOSTS' }
+ let(:ssh_auth) { Gitlab::Git::SshAuth.new(ssh_key, known_hosts) }
+ let(:gl_projects_timeout) { Gitlab::Git::RepositoryMirroring::GITLAB_PROJECTS_TIMEOUT }
+ let(:gl_projects_force) { true }
+ let(:env) { { 'GIT_SSH_COMMAND' => /ssh/ } }
+
+ subject(:remote_mirror) do
+ described_class.new(
+ repository,
+ ref_name,
+ ssh_auth: ssh_auth,
+ only_branches_matching: []
+ )
+ end
+
+ def ref(name)
+ double("ref-#{name}", name: name, dereferenced_target: double(id: name))
+ end
+
+ describe '#update' do
+ it 'updates the remote repository' do
+ # Stub this check so we try to delete the obsolete refs
+ allow(repository).to receive(:ancestor?).and_return(true)
+
+ expect(repository).to receive(:local_branches).and_return([ref('master'), ref('new-branch')])
+ expect(repository).to receive(:remote_branches)
+ .with(ref_name)
+ .and_return([ref('master'), ref('obsolete-branch')])
+
+ expect(repository).to receive(:tags).and_return([ref('v1.0.0'), ref('new-tag')])
+ expect(repository).to receive(:remote_tags)
+ .with(ref_name, env: env)
+ .and_return([ref('v1.0.0'), ref('obsolete-tag')])
+
+ expect(gitlab_projects)
+ .to receive(:push_branches)
+ .with(ref_name, gl_projects_timeout, gl_projects_force, ['master', 'new-branch'], env: env)
+ .and_return(true)
+
+ expect(gitlab_projects)
+ .to receive(:push_branches)
+ .with(ref_name, gl_projects_timeout, gl_projects_force, ['v1.0.0', 'new-tag'], env: env)
+ .and_return(true)
+
+ expect(gitlab_projects)
+ .to receive(:delete_remote_branches)
+ .with(ref_name, ['obsolete-branch'], env: env)
+ .and_return(true)
+
+ expect(gitlab_projects)
+ .to receive(:delete_remote_branches)
+ .with(ref_name, ['obsolete-tag'], env: env)
+ .and_return(true)
+
+ remote_mirror.update
+ end
+ end
+end
diff --git a/ruby/spec/lib/gitlab/git/ssh_auth_spec.rb b/ruby/spec/lib/gitlab/git/ssh_auth_spec.rb
new file mode 100644
index 000000000..2a2458181
--- /dev/null
+++ b/ruby/spec/lib/gitlab/git/ssh_auth_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe Gitlab::Git::SshAuth do
+ describe '.from_gitaly' do
+ it 'initializes based on ssh_key and known_hosts in the request' do
+ result = described_class.from_gitaly(double(ssh_key: 'SSH KEY', known_hosts: 'KNOWN HOSTS'))
+
+ expect(result.class).to eq(described_class)
+ expect(result.ssh_key).to eq('SSH KEY')
+ expect(result.known_hosts).to eq('KNOWN HOSTS')
+ end
+ end
+
+ describe '#setup' do
+ subject { described_class.new(ssh_key, known_hosts).setup { |env| env } }
+
+ context 'no credentials' do
+ let(:ssh_key) { nil }
+ let(:known_hosts) { nil }
+
+ it 'writes no tempfiles' do
+ expect(Tempfile).not_to receive(:new)
+
+ is_expected.to eq({})
+ end
+ end
+
+ context 'just the SSH key' do
+ let(:ssh_key) { 'Fake SSH key' }
+ let(:known_hosts) { nil }
+
+ it 'writes the SSH key file' do
+ ssh_key_file = stub_tempfile('/tmp files/keyFile', 'gitlab-shell-key-file', chmod: 0o400)
+
+ is_expected.to eq(build_env(ssh_key_file: ssh_key_file.path))
+
+ expect(ssh_key_file.string).to eq(ssh_key)
+ end
+ end
+
+ context 'just the known_hosts file' do
+ let(:ssh_key) { nil }
+ let(:known_hosts) { 'Fake known_hosts data' }
+
+ it 'writes the known_hosts file and script' do
+ known_hosts_file = stub_tempfile('/tmp files/knownHosts', 'gitlab-shell-known-hosts', chmod: 0o400)
+
+ is_expected.to eq(build_env(known_hosts_file: known_hosts_file.path))
+
+ expect(known_hosts_file.string).to eq(known_hosts)
+ end
+ end
+
+ context 'SSH key and known_hosts file' do
+ let(:ssh_key) { 'Fake SSH key' }
+ let(:known_hosts) { 'Fake known_hosts data' }
+
+ it 'writes SSH key, known_hosts and script files' do
+ ssh_key_file = stub_tempfile('id_rsa', 'gitlab-shell-key-file', chmod: 0o400)
+ known_hosts_file = stub_tempfile('known_hosts', 'gitlab-shell-known-hosts', chmod: 0o400)
+
+ is_expected.to eq(build_env(ssh_key_file: ssh_key_file.path, known_hosts_file: known_hosts_file.path))
+
+ expect(ssh_key_file.string).to eq(ssh_key)
+ expect(known_hosts_file.string).to eq(known_hosts)
+ end
+ end
+ end
+
+ def build_env(ssh_key_file: nil, known_hosts_file: nil)
+ opts = []
+
+ if ssh_key_file
+ opts << %('-oIdentityFile="#{ssh_key_file}"')
+ opts << %q('-oIdentitiesOnly="yes"')
+ end
+
+ if known_hosts_file
+ opts << %q('-oStrictHostKeyChecking="yes"')
+ opts << %('-oUserKnownHostsFile="#{known_hosts_file}"')
+ end
+
+ { 'GIT_SSH_COMMAND' => %(ssh #{opts.join(' ')}) }
+ end
+
+ def stub_tempfile(name, filename, chmod:)
+ file = StringIO.new
+
+ allow(file).to receive(:path).and_return(name)
+
+ expect(Tempfile).to receive(:new).with(filename).and_return(file)
+ expect(file).to receive(:chmod).with(chmod)
+ expect(file).to receive(:close!)
+
+ file
+ end
+end
diff --git a/ruby/spec/test_repo_helper.rb b/ruby/spec/test_repo_helper.rb
index c013d8caa..411f0bf40 100644
--- a/ruby/spec/test_repo_helper.rb
+++ b/ruby/spec/test_repo_helper.rb
@@ -74,16 +74,27 @@ module TestRepo
File.join(DEFAULT_STORAGE_DIR, gitaly_repo.relative_path)
end
- def gitlab_git_from_gitaly(gitaly_repo)
+ def gitlab_git_from_gitaly(gitaly_repo, gitlab_projects: nil)
Gitlab::Git::Repository.new(
gitaly_repo,
repo_path_from_gitaly(gitaly_repo),
'',
- nil,
+ gitlab_projects,
''
)
end
+ def gitlab_git_from_gitaly_with_gitlab_projects(gitaly_repo)
+ gitlab_projects = Gitlab::Git::GitlabProjects.new(
+ DEFAULT_STORAGE_DIR,
+ gitaly_repo.relative_path,
+ global_hooks_path: '',
+ logger: Rails.logger
+ )
+
+ gitlab_git_from_gitaly(gitaly_repo, gitlab_projects: gitlab_projects)
+ end
+
def repository_from_relative_path(relative_path)
gitlab_git_from_gitaly(
Gitaly::Repository.new(storage_name: DEFAULT_STORAGE_NAME, relative_path: relative_path)