Skip to content
Snippets Groups Projects
Commit 7559ccc7 authored by Tim Zallmann's avatar Tim Zallmann
Browse files

Merge branch '98-pull-mirror-ssh-keys-2' into 'master'

Resolve "Pull repository mirroring: Support for SSH keys"

Closes #98

See merge request !2551
parents 5fa10409 6b867eb8
No related branches found
No related tags found
1 merge request!2551Resolve "Pull repository mirroring: Support for SSH keys"
Pipeline #
Showing
with 482 additions and 30 deletions
Loading
Loading
@@ -405,6 +405,17 @@ gem 'sys-filesystem', '~> 1.1.6'
# NTP client
gem 'net-ntp'
 
# SSH host key support
gem 'net-ssh', '~> 4.1.0'
gem 'sshkey', '~> 1.9.0'
# Required for ED25519 SSH host key support
group :ed25519 do
gem 'rbnacl-libsodium'
gem 'rbnacl', '~> 3.2'
gem 'bcrypt_pbkdf', '~> 1.0'
end
# Gitaly GRPC client
gem 'gitaly', '~> 0.24.0'
 
Loading
Loading
Loading
Loading
@@ -83,6 +83,7 @@ GEM
babosa (1.0.2)
base32 (0.3.2)
bcrypt (3.1.11)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
better_errors (2.1.1)
coderay (>= 1.0.0)
Loading
Loading
@@ -504,6 +505,7 @@ GEM
mysql2 (0.4.5)
net-ldap (0.16.0)
net-ntp (2.1.3)
net-ssh (4.1.0)
netrc (0.11.0)
nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
Loading
Loading
@@ -691,6 +693,10 @@ GEM
rake (12.0.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rbnacl (3.4.0)
ffi
rbnacl-libsodium (1.0.11)
rbnacl (>= 3.0.1)
rdoc (4.2.2)
json (~> 1.4)
re2 (1.1.1)
Loading
Loading
@@ -856,6 +862,7 @@ GEM
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
sshkey (1.9.0)
stackprof (0.2.10)
state_machines (0.4.0)
state_machines-activemodel (0.4.0)
Loading
Loading
@@ -954,6 +961,7 @@ DEPENDENCIES
aws-sdk
babosa (~> 1.0.2)
base32 (~> 0.3.0)
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
Loading
Loading
@@ -1053,6 +1061,7 @@ DEPENDENCIES
mysql2 (~> 0.4.5)
net-ldap
net-ntp
net-ssh (~> 4.1.0)
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.4)
octokit (~> 4.6.2)
Loading
Loading
@@ -1099,6 +1108,8 @@ DEPENDENCIES
rainbow (~> 2.2)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
rbnacl (~> 3.2)
rbnacl-libsodium
rdoc (~> 4.2)
re2 (~> 1.1.1)
recaptcha (~> 3.0)
Loading
Loading
@@ -1141,6 +1152,7 @@ DEPENDENCIES
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0)
sprockets (~> 3.7.0)
sshkey (~> 1.9.0)
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
Loading
Loading
export default {
PASSWORD: 'password',
SSH: 'ssh_public_key',
};
import MirrorPull from './mirror_pull';
document.addEventListener('DOMContentLoaded', () => {
const mirrorPull = new MirrorPull('.js-project-mirror-push-form');
mirrorPull.init();
});
/* global Flash */
import AUTH_METHOD from './constants';
export default class MirrorPull {
constructor(formSelector) {
this.backOffRequestCounter = 0;
this.$form = $(formSelector);
this.$repositoryUrl = this.$form.find('.js-repo-url');
this.$sectionSSHHostKeys = this.$form.find('.js-ssh-host-keys-section');
this.$hostKeysInformation = this.$form.find('.js-fingerprint-ssh-info');
this.$btnDetectHostKeys = this.$form.find('.js-detect-host-keys');
this.$btnSSHHostsShowAdvanced = this.$form.find('.btn-show-advanced');
this.$dropdownAuthType = this.$form.find('.js-pull-mirror-auth-type');
this.$wellAuthTypeChanging = this.$form.find('.js-well-changing-auth');
this.$wellPasswordAuth = this.$form.find('.js-well-password-auth');
this.$wellSSHAuth = this.$form.find('.js-well-ssh-auth');
this.$sshPublicKeyWrap = this.$form.find('.js-ssh-public-key-wrap');
}
init() {
this.toggleAuthWell(this.$dropdownAuthType.val());
this.$repositoryUrl.on('keyup', e => this.handleRepositoryUrlInput(e));
this.$form.find('.js-known-hosts').on('keyup', e => this.handleSSHKnownHostsInput(e));
this.$dropdownAuthType.on('change', e => this.handleAuthTypeChange(e));
this.$btnDetectHostKeys.on('click', e => this.handleDetectHostKeys(e));
this.$btnSSHHostsShowAdvanced.on('click', e => this.handleSSHHostsAdvanced(e));
}
/**
* Method to monitor Git Repository URL input
*/
handleRepositoryUrlInput() {
const protocol = this.$repositoryUrl.val().split('://')[0];
const protRegEx = /http|git/;
// Validate URL and verify if it consists only supported protocols
if (this.$form.get(0).checkValidity()) {
// Hide/Show SSH Host keys section only for SSH URLs
this.$sectionSSHHostKeys.toggleClass('hidden', protocol !== 'ssh');
this.$btnDetectHostKeys.enable();
// Verify if URL is http, https or git and hide/show Auth type dropdown
// as we don't support auth type SSH for non-SSH URLs
this.$dropdownAuthType.toggleClass('hidden', protRegEx.test(protocol));
}
}
/**
* Click event handler to detect SSH Host key and fingerprints from
* provided Git Repository URL.
*/
handleDetectHostKeys() {
const projectMirrorSSHEndpoint = this.$form.data('project-mirror-endpoint');
const repositoryUrl = this.$repositoryUrl.val();
const $btnLoadSpinner = this.$btnDetectHostKeys.find('.detect-host-keys-load-spinner');
// Disable button while we make request
this.$btnDetectHostKeys.disable();
$btnLoadSpinner.removeClass('hidden');
// Make backOff polling to get data
gl.utils.backOff((next, stop) => {
$.getJSON(`${projectMirrorSSHEndpoint}?ssh_url=${repositoryUrl}`)
.done((res, statusText, header) => {
if (header.status === 204) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
stop(res);
}
} else {
stop(res);
}
})
.fail(stop);
})
.then((res) => {
$btnLoadSpinner.addClass('hidden');
// Once data is received, we show verification info along with Host keys and fingerprints
this.$hostKeysInformation.find('.js-fingerprint-verification').toggleClass('hidden', res.changes_project_import_data);
if (res.known_hosts && res.fingerprints) {
this.showSSHInformation(res);
}
})
.catch((res) => {
// Show failure message when there's an error and re-enable Detect host keys button
const failureMessage = res.responseJSON ? res.responseJSON.message : 'Something went wrong on our end.';
Flash(failureMessage); // eslint-disable-line
$btnLoadSpinner.addClass('hidden');
this.$btnDetectHostKeys.enable();
});
}
/**
* Method to monitor known hosts textarea input
*/
handleSSHKnownHostsInput() {
// Strike-out fingerprints and remove verification info if `known hosts` value is altered
this.$hostKeysInformation.find('.js-fingerprints-list').addClass('invalidate');
this.$hostKeysInformation.find('.js-fingerprint-verification').addClass('hidden');
}
/**
* Click event handler for `Show advanced` button under SSH Host keys section
*/
handleSSHHostsAdvanced() {
const $knownHost = this.$sectionSSHHostKeys.find('.js-ssh-known-hosts');
$knownHost.toggleClass('hidden');
this.$btnSSHHostsShowAdvanced.toggleClass('show-advanced', $knownHost.hasClass('hidden'));
}
/**
* Authentication method dropdown change event listener
*/
handleAuthTypeChange() {
const projectMirrorAuthTypeEndpoint = `${this.$form.attr('action')}.json`;
const $sshPublicKey = this.$sshPublicKeyWrap.find('.ssh-public-key');
const selectedAuthType = this.$dropdownAuthType.val();
// Construct request body
const authTypeData = {
project: {
import_data_attributes: {
regenerate_ssh_private_key: true,
},
},
};
// Show load spinner and hide other containers
this.$wellAuthTypeChanging.removeClass('hidden');
this.$wellPasswordAuth.addClass('hidden');
this.$wellSSHAuth.addClass('hidden');
// This request should happen only if selected Auth type was SSH
// and SSH Public key was not present on page load
if (selectedAuthType === AUTH_METHOD.SSH &&
!$sshPublicKey.text().trim()) {
this.$dropdownAuthType.disable();
$.ajax({
type: 'PUT',
url: projectMirrorAuthTypeEndpoint,
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(authTypeData),
})
.done((res) => {
// Show SSH public key container and fill in public key
this.toggleAuthWell(selectedAuthType);
this.toggleSSHAuthWellMessage(true);
$sshPublicKey.text(res.import_data_attributes.ssh_public_key);
})
.fail(() => {
Flash('Something went wrong on our end.');
})
.always(() => {
this.$wellAuthTypeChanging.addClass('hidden');
this.$dropdownAuthType.enable();
});
} else {
this.$wellAuthTypeChanging.addClass('hidden');
this.toggleAuthWell(selectedAuthType);
this.$wellSSHAuth.find('.js-ssh-public-key-present').removeClass('hidden');
}
}
/**
* Method to parse SSH Host keys data and render it
* under SSH host keys section
*/
showSSHInformation(sshHostKeys) {
const $fingerprintsList = this.$hostKeysInformation.find('.js-fingerprints-list');
let fingerprints = '';
sshHostKeys.fingerprints.forEach((fingerprint) => {
const escFingerprints = _.escape(fingerprint.fingerprint);
fingerprints += `<code>${escFingerprints}</code>`;
});
this.$hostKeysInformation.removeClass('hidden');
$fingerprintsList.removeClass('invalidate');
$fingerprintsList.html(fingerprints);
this.$sectionSSHHostKeys.find('.js-known-hosts').val(sshHostKeys.known_hosts);
}
/**
* Toggle Auth type information container based on provided `authType`
*/
toggleAuthWell(authType) {
this.$wellPasswordAuth.toggleClass('hidden', authType !== AUTH_METHOD.PASSWORD);
this.$wellSSHAuth.toggleClass('hidden', authType !== AUTH_METHOD.SSH);
}
/**
* Toggle SSH auth information message
*/
toggleSSHAuthWellMessage(sshKeyPresent) {
this.$sshPublicKeyWrap.toggleClass('hidden', !sshKeyPresent);
this.$wellSSHAuth.find('.js-ssh-public-key-present').toggleClass('hidden', !sshKeyPresent);
this.$wellSSHAuth.find('.js-btn-regenerate-ssh-key').toggleClass('hidden', !sshKeyPresent);
this.$wellSSHAuth.find('.js-ssh-public-key-pending').toggleClass('hidden', sshKeyPresent);
}
}
Loading
Loading
@@ -990,3 +990,76 @@ a.allowed-to-push {
border-color: $border-color;
}
}
/* EE-specific styles */
.project-mirror-settings {
.fingerprint-verified {
color: $green-500;
}
.ssh-public-key,
.btn-copy-ssh-public-key {
float: left;
}
.ssh-public-key {
width: 95%;
word-wrap: break-word;
word-break: break-all;
}
.btn-copy-ssh-public-key {
margin-left: 5px;
}
.known-hosts {
font-family: $monospace_font;
}
.btn-show-advanced {
min-width: 135px;
.label-show {
display: none;
}
.label-hide {
display: inline;
}
.fa.fa-chevron::before {
content: "\f077";
}
&.show-advanced {
.label-show {
display: inline;
}
.label-hide {
display: none;
}
.fa.fa-chevron::before {
content: "\f078";
}
}
}
.fingerprints-list {
code {
display: block;
padding: 8px;
margin-bottom: 5px;
}
&.invalidate {
text-decoration: line-through;
}
}
.changing-auth-method {
display: flex;
justify-content: center;
}
}
Loading
Loading
@@ -13,6 +13,22 @@ def show
redirect_to_repository_settings(@project)
end
 
def ssh_host_keys
lookup = SshHostKey.new(project: project, url: params[:ssh_url])
if lookup.error.present?
# Failed to read keys
render json: { message: lookup.error }, status: 400
elsif lookup.known_hosts.nil?
# Still working, come back later
render body: nil, status: 204
else
render json: lookup
end
rescue ArgumentError => err
render json: { message: err.message }, status: 400
end
def update
if @project.update_attributes(safe_mirror_params)
if @project.mirror?
Loading
Loading
@@ -28,7 +44,17 @@ def update
flash[:alert] = @project.errors.full_messages.join(', ').html_safe
end
 
redirect_to_repository_settings(@project)
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.json do
if @project.errors.present?
render json: @project.errors, status: :unprocessable_entity
else
render json: ProjectMirrorSerializer.new.represent(@project)
end
end
end
end
 
def update_now
Loading
Loading
@@ -50,13 +76,35 @@ def remote_mirror
end
 
def mirror_params
params.require(:project).permit(:mirror, :import_url, :mirror_user_id,
:mirror_trigger_builds, remote_mirrors_attributes: [:url, :id, :enabled])
params.require(:project)
.permit(
:mirror,
:import_url,
:username_only_import_url,
:mirror_user_id,
:mirror_trigger_builds,
import_data_attributes: [:id, :auth_method, :password, :ssh_known_hosts, :regenerate_ssh_private_key],
remote_mirrors_attributes: [:url, :id, :enabled]
)
end
 
def safe_mirror_params
return mirror_params if valid_mirror_user?(mirror_params)
params = mirror_params
params[:mirror_user_id] = current_user.id unless valid_mirror_user?(params)
import_data = params[:import_data_attributes]
if import_data.present?
# Prevent Rails from destroying the existing import data
import_data[:id] ||= project.import_data&.id
# If the known hosts data is being set, store details about who and when
if import_data[:ssh_known_hosts].present?
import_data[:ssh_known_hosts_verified_at] = Time.now
import_data[:ssh_known_hosts_verified_by_id] = current_user.id
end
end
 
mirror_params.merge(mirror_user_id: current_user.id)
params
end
end
Loading
Loading
@@ -167,7 +167,7 @@ def set_last_repository_updated_at
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
 
has_one :import_data, class_name: 'ProjectImportData'
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature
has_one :statistics, class_name: 'ProjectStatistics'
 
Loading
Loading
@@ -196,6 +196,7 @@ def set_last_repository_updated_at
 
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature
accepts_nested_attributes_for :import_data
 
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :count, to: :forks, prefix: true
Loading
Loading
@@ -587,8 +588,6 @@ def create_or_update_import_data(data: nil, credentials: nil)
project_import_data.credentials ||= {}
project_import_data.credentials = project_import_data.credentials.merge(credentials)
end
project_import_data.save
end
 
def import?
Loading
Loading
require 'carrierwave/orm/activerecord'
 
class ProjectImportData < ActiveRecord::Base
belongs_to :project
prepend ::EE::ProjectImportData
belongs_to :project, inverse_of: :import_data
attr_encrypted :credentials,
key: Gitlab::Application.secrets.db_key_base,
marshal: true,
Loading
Loading
Loading
Loading
@@ -966,7 +966,7 @@ def merged_to_root_ref?(branch_name)
 
def fetch_upstream(url)
add_remote(Repository::MIRROR_REMOTE, url)
fetch_remote(Repository::MIRROR_REMOTE)
fetch_remote(Repository::MIRROR_REMOTE, ssh_auth: project&.import_data)
end
 
def fetch_geo_mirror(url)
Loading
Loading
@@ -1088,8 +1088,8 @@ def remove_remote(name)
false
end
 
def fetch_remote(remote, forced: false, no_tags: false)
gitlab_shell.fetch_remote(repository_storage_path, disk_path, remote, forced: forced, no_tags: no_tags)
def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false)
gitlab_shell.fetch_remote(repository_storage_path, disk_path, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
end
 
def fetch_ref(source_path, source_ref, target_ref)
Loading
Loading
.account-well.prepend-top-default.append-bottom-default
%ul
%li
The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.
The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> or <code>git://</code>.
%li
If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.
%li
The update action will time out after 10 minutes. For big repositories, use a clone/push combination.
%li
Loading
Loading
- expanded = Rails.env.test?
- import_data = @project.import_data || @project.build_import_data
- protocols = AddressableUrlValidator::DEFAULT_OPTIONS[:protocols].join('|')
%section.settings.project-mirror-settings
.settings-header
%h4
Loading
Loading
@@ -10,30 +13,26 @@
updated from an upstream repository.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pulling-from-a-remote-repository'), target: '_blank'
.settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for @project, url: project_mirror_path(@project) do |f|
= form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors project-mirror-push-form js-project-mirror-push-form', autocomplete: 'false', data: { project_mirror_endpoint: ssh_host_keys_project_mirror_path(@project, :json) } } do |f|
%div
= form_errors(@project)
%h5
Set up mirror repository
= render "shared/mirror_update_button"
- if @project.mirror_last_update_failed?
.panel.panel-danger
.panel-heading
The repository failed to update #{time_ago_with_tooltip(@project.mirror_last_update_at)}.
- if @project.mirror_ever_updated_successfully?
Last successful update #{time_ago_with_tooltip(@project.mirror_last_successful_update_at)}.
.panel-body
%pre
:preserve
#{h(@project.import_error.try(:strip))}
= render "projects/mirrors/pull/mirror_update_fail"
.form-group
= f.check_box :mirror, class: "pull-left"
.prepend-left-20
= f.label :mirror, "Mirror repository", class: "label-light append-bottom-0"
.form-group
= f.label :import_url, "Git repository URL", class: "label-light"
= f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
= render "projects/mirrors/instructions"
= f.label :username_only_import_url, "Git repository URL", class: "label-light"
= f.text_field :username_only_import_url, class: 'form-control js-repo-url', placeholder: 'https://username@gitlab.company.com/group/project.git', required: 'required', pattern: "(#{protocols}):\/\/.+", title: 'URL must have protocol present (eg; ssh://...)'
= render "projects/mirrors/instructions"
= f.fields_for :import_data, import_data do |import_form|
= render partial: "projects/mirrors/pull/ssh_host_keys", locals: { f: import_form }
= render partial: "projects/mirrors/pull/authentication_method", locals: { f: import_form }
.form-group
= f.label :mirror_user_id, "Mirror user", class: "label-light"
= select_tag('project[mirror_user_id]', options_for_mirror_user, class: "select2 lg", required: true)
Loading
Loading
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'mirrors'
- if @project.feature_available?(:repository_mirrors)
= render 'projects/mirrors/pull'
= render 'projects/mirrors/push'
- import_data = f.object
- regen_data = { auth_method: 'ssh_public_key', regenerate_ssh_private_key: true }
- ssh_key_auth = import_data.ssh_key_auth?
- ssh_public_key_present = import_data.ssh_public_key.present?
.form-group
= f.label :auth_method, 'Authentication method', class: 'label-light'
= f.select :auth_method,
options_for_select([['Password authentication', 'password'], ['SSH public key authentication', 'ssh_public_key']], import_data.auth_method),
{}, { class: "form-control js-pull-mirror-auth-type #{'hidden' unless import_data.ssh_import?}" }
.form-group
.account-well.changing-auth-method.hidden.js-well-changing-auth
= icon('spinner spin lg')
.account-well.well-password-auth.hidden.js-well-password-auth
= f.label :password, "Password", class: "label-light"
= f.password_field :password, value: import_data.password, class: 'form-control'
.account-well.well-ssh-auth.hidden.js-well-ssh-auth
%p.js-ssh-public-key-present{ class: ('hidden' unless ssh_public_key_present) }
Here is the public SSH key that needs to be added to the remote
server. For more information, please refer to the documentation.
%p.js-ssh-public-key-pending{ class: ('hidden' if ssh_public_key_present) }
An SSH key will be automatically generated when the form is
submitted. For more information, please refer to the documentation.
.clearfix.js-ssh-public-key-wrap{ class: ('hidden' unless ssh_public_key_present) }
%code.prepend-top-10.ssh-public-key
= import_data.ssh_public_key
= clipboard_button(text: import_data.ssh_public_key, title: _("Copy SSH public key to clipboard"), class: 'prepend-top-10 btn-copy-ssh-public-key')
= link_to 'Regenerate key', project_mirror_path(@project, project: { import_data_attributes: regen_data }),
method: :patch,
data: { confirm: 'Are you sure you want to regenerate public key? You will have to update the public key on the remote server before mirroring will work again.' },
class: "btn btn-inverted btn-warning prepend-top-10 js-btn-regenerate-ssh-key #{ 'hidden' unless ssh_public_key_present }"
- if @project.mirror_last_update_failed?
.panel.panel-danger
.panel-heading
The repository failed to update #{time_ago_with_tooltip(@project.mirror_last_update_at)}.
- if @project.mirror_ever_updated_successfully?
Last successful update #{time_ago_with_tooltip(@project.mirror_last_successful_update_at)}.
.panel-body
%pre
:preserve
#{h(@project.import_error.try(:strip))}
- import_data = f.object
- verified_by = import_data.ssh_known_hosts_verified_by
- verified_at = import_data.ssh_known_hosts_verified_at
.form-group.js-ssh-host-keys-section{ class: ('hidden' unless import_data.ssh_import?) }
%button.btn.btn-inverted.btn-success.append-bottom-15.js-detect-host-keys{ type: 'button' }
= icon('spinner spin', class: 'detect-host-keys-load-spinner hidden')
Detect host keys
.fingerprint-ssh-info.js-fingerprint-ssh-info{ class: ('hidden' unless import_data.ssh_import?) }
%label.label-light
Fingerprints
.fingerprints-list.js-fingerprints-list
- import_data.ssh_known_hosts_fingerprints.each do |fp|
%code= fp.fingerprint
- if verified_by || verified_at
.help-block.js-fingerprint-verification
%i.fa.fa-check.fingerprint-verified
Verified by
- if verified_by
= link_to verified_by.name, user_path(verified_by)
- else
a deleted user
#{time_ago_in_words(verified_at)} ago
.js-ssh-hosts-advanced
%button.btn.btn-sm.btn-default.prepend-top-10.append-bottom-15.btn-show-advanced.show-advanced{ type: 'button' }
%span.label-show
Show advanced
%span.label-hide
Hide advanced
= icon('chevron')
.js-ssh-known-hosts.hidden
= f.label :ssh_known_hosts, 'SSH host keys', class: 'label-light'
= f.text_area :ssh_known_hosts, class: 'form-control known-hosts js-known-hosts', rows: '10'
Loading
Loading
@@ -16,6 +16,9 @@
To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}.
%li
The Git LFS objects will <strong>not</strong> be imported.
%li
Once imported, repositories can be mirrored over SSH. Read more
= link_to 'here', help_page_path('/workflow/repository_mirroring.md', anchor: 'ssh-authentication')
 
.form-group
.col-sm-offset-2.col-sm-10
Loading
Loading
---
title: Implement SSH public-key support for repository mirroring
merge_request: 2423
author:
Loading
Loading
@@ -44,6 +44,7 @@ class Application < Rails::Application
#{config.root}/ee/app/models
#{config.root}/ee/app/models/concerns
#{config.root}/ee/app/policies
#{config.root}/ee/app/serializers
#{config.root}/ee/app/services
#{config.root}/ee/app/workers
])
Loading
Loading
Loading
Loading
@@ -189,6 +189,7 @@
## EE-specific
resource :mirror, only: [:show, :update] do
member do
get :ssh_host_keys, constraints: { format: :json }
post :update_now
end
end
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment