diff options
author | Nick Thomas <nick@gitlab.com> | 2018-11-05 15:58:44 +0300 |
---|---|---|
committer | Nick Thomas <nick@gitlab.com> | 2018-11-09 18:27:41 +0300 |
commit | ef3679992eaa56a85cde6c9fd50ec41cf733f395 (patch) | |
tree | a916e68050ec0755286ec9f9903379dbc8c4e9e9 | |
parent | 1a4581d04d67d2821e5df6866af8ba6bbef40960 (diff) |
Support SSH credentials for push mirroring
-rw-r--r-- | changelogs/unreleased/ssh-credentials-for-remote-mirroring.yml | 5 | ||||
-rw-r--r-- | ruby/lib/gitaly_server/remote_service.rb | 10 | ||||
-rw-r--r-- | ruby/lib/gitaly_server/repository_service.rb | 14 | ||||
-rw-r--r-- | ruby/lib/gitlab/git/gitlab_projects.rb | 72 | ||||
-rw-r--r-- | ruby/lib/gitlab/git/remote_mirror.rb | 67 | ||||
-rw-r--r-- | ruby/lib/gitlab/git/repository.rb | 13 | ||||
-rw-r--r-- | ruby/lib/gitlab/git/repository_mirroring.rb | 26 | ||||
-rw-r--r-- | ruby/lib/gitlab/git/ssh_auth.rb | 73 | ||||
-rw-r--r-- | ruby/spec/gitaly/remote_service_spec.rb | 45 | ||||
-rw-r--r-- | ruby/spec/gitaly/repository_service_spec.rb | 45 | ||||
-rw-r--r-- | ruby/spec/lib/gitlab/git/gitlab_projects_spec.rb | 91 | ||||
-rw-r--r-- | ruby/spec/lib/gitlab/git/remote_mirror_spec.rb | 67 | ||||
-rw-r--r-- | ruby/spec/lib/gitlab/git/ssh_auth_spec.rb | 97 | ||||
-rw-r--r-- | ruby/spec/test_repo_helper.rb | 15 |
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) |