Commit bd659f70 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'fj-6860-instance-level-project-templates' into 'master'

[CE Port]: Implement instance level project templates

See merge request gitlab-org/gitlab-ce!20761
parents e53e4d45 60943a60
...@@ -2,21 +2,27 @@ ...@@ -2,21 +2,27 @@
module Projects module Projects
class CreateFromTemplateService < BaseService class CreateFromTemplateService < BaseService
include Gitlab::Utils::StrongMemoize
def initialize(user, params) def initialize(user, params)
@current_user, @params = user, params.dup @current_user, @params = user, params.dup
end end
def execute def execute
template_name = params.delete(:template_name) file = Gitlab::ProjectTemplate.find(template_name)&.file
file = Gitlab::ProjectTemplate.find(template_name).file
override_params = params.dup override_params = params.dup
params[:file] = file params[:file] = file
GitlabProjectsImportService.new(current_user, params, override_params).execute GitlabProjectsImportService.new(current_user, params, override_params).execute
ensure ensure
file&.close file&.close
end end
def template_name
strong_memoize(:template_name) do
params.delete(:template_name).presence
end
end
end end
end end
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
# The latter will under the hood just import an archive supplied by GitLab. # The latter will under the hood just import an archive supplied by GitLab.
module Projects module Projects
class GitlabProjectsImportService class GitlabProjectsImportService
include Gitlab::Utils::StrongMemoize
include Gitlab::TemplateHelper
attr_reader :current_user, :params attr_reader :current_user, :params
def initialize(user, import_params, override_params = nil) def initialize(user, import_params, override_params = nil)
...@@ -12,47 +15,56 @@ module Projects ...@@ -12,47 +15,56 @@ module Projects
end end
def execute def execute
FileUtils.mkdir_p(File.dirname(import_upload_path)) prepare_template_environment(template_file&.path)
file = params.delete(:file) prepare_import_params
FileUtils.copy_entry(file.path, import_upload_path)
@overwrite = params.delete(:overwrite) ::Projects::CreateService.new(current_user, params).execute
data = {}
data[:override_params] = @override_params if @override_params
if overwrite_project?
data[:original_path] = params[:path]
params[:path] += "-#{tmp_filename}"
end end
params[:import_type] = 'gitlab_project' private
params[:import_source] = import_upload_path
params[:import_data] = { data: data } if data.present?
::Projects::CreateService.new(current_user, params).execute def overwrite_project?
overwrite? && project_with_same_full_path?
end end
private def project_with_same_full_path?
Project.find_by_full_path("#{current_namespace.full_path}/#{params[:path]}").present?
end
def import_upload_path def current_namespace
@import_upload_path ||= Gitlab::ImportExport.import_upload_path(filename: tmp_filename) strong_memoize(:current_namespace) do
Namespace.find_by(id: params[:namespace_id])
end
end end
def tmp_filename def overwrite?
SecureRandom.hex strong_memoize(:overwrite) do
params.delete(:overwrite)
end
end end
def overwrite_project? def template_file
@overwrite && project_with_same_full_path? strong_memoize(:template_file) do
params.delete(:file)
end
end end
def project_with_same_full_path? def prepare_import_params
Project.find_by_full_path("#{current_namespace.full_path}/#{params[:path]}").present? data = {}
data[:override_params] = @override_params if @override_params
if overwrite_project?
data[:original_path] = params[:path]
params[:path] += "-#{tmp_filename}"
end end
def current_namespace if template_file
@current_namespace ||= Namespace.find_by(id: params[:namespace_id]) params[:import_type] = 'gitlab_project'
params[:import_source] = import_upload_path
end
params[:import_data] = { data: data } if data.present?
end end
end end
end end
...@@ -7,9 +7,9 @@ class RepositoryImportWorker ...@@ -7,9 +7,9 @@ class RepositoryImportWorker
include ProjectImportOptions include ProjectImportOptions
def perform(project_id) def perform(project_id)
project = Project.find(project_id) @project = Project.find(project_id)
return unless start_import(project) return unless start_import
Gitlab::Metrics.add_event(:import_repository) Gitlab::Metrics.add_event(:import_repository)
...@@ -21,7 +21,7 @@ class RepositoryImportWorker ...@@ -21,7 +21,7 @@ class RepositoryImportWorker
return if service.async? return if service.async?
if result[:status] == :error if result[:status] == :error
fail_import(project, result[:message]) if project.gitlab_project_import? fail_import(result[:message]) if template_import?
raise result[:message] raise result[:message]
end end
...@@ -31,14 +31,20 @@ class RepositoryImportWorker ...@@ -31,14 +31,20 @@ class RepositoryImportWorker
private private
def start_import(project) attr_reader :project
def start_import
return true if start(project) return true if start(project)
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.") Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false false
end end
def fail_import(project, message) def fail_import(message)
project.mark_import_as_failed(message) project.mark_import_as_failed(message)
end end
def template_import?
project.gitlab_project_import?
end
end end
...@@ -106,6 +106,7 @@ created in snippets, wikis, and repos. ...@@ -106,6 +106,7 @@ created in snippets, wikis, and repos.
- [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service. - [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service.
- [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project. - [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project.
- [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet. - [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet.
- [Custom project templates](https://docs.gitlab.com/ee/user/admin_area/custom_project_templates.html): Configure a set of projects to be used as custom templates when creating a new project. **[PREMIUM ONLY]**
### Repository settings ### Repository settings
......
...@@ -22,24 +22,28 @@ module Gitlab ...@@ -22,24 +22,28 @@ module Gitlab
class << self class << self
def options def options
@options ||= Hash[ImportTable.map { |importer| [importer.title, importer.name] }] Hash[import_table.map { |importer| [importer.title, importer.name] }]
end end
def values def values
@values ||= ImportTable.map(&:name) import_table.map(&:name)
end end
def importer_names def importer_names
@importer_names ||= ImportTable.select(&:importer).map(&:name) import_table.select(&:importer).map(&:name)
end end
def importer(name) def importer(name)
ImportTable.find { |import_source| import_source.name == name }.importer import_table.find { |import_source| import_source.name == name }.importer
end end
def title(name) def title(name)
options.key(name) options.key(name)
end end
def import_table
ImportTable
end
end end
end end
end end
module Gitlab
module TemplateHelper
include Gitlab::Utils::StrongMemoize
def prepare_template_environment(file_path)
return unless file_path.present?
FileUtils.mkdir_p(File.dirname(import_upload_path))
FileUtils.copy_entry(file_path, import_upload_path)
end
def import_upload_path
strong_memoize(:import_upload_path) do
Gitlab::ImportExport.import_upload_path(filename: tmp_filename)
end
end
def tmp_filename
SecureRandom.hex
end
end
end
...@@ -2,10 +2,11 @@ require 'spec_helper' ...@@ -2,10 +2,11 @@ require 'spec_helper'
describe Projects::CreateFromTemplateService do describe Projects::CreateFromTemplateService do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:template_name) { 'rails' }
let(:project_params) do let(:project_params) do
{ {
path: user.to_param, path: user.to_param,
template_name: 'rails', template_name: template_name,
description: 'project description', description: 'project description',
visibility_level: Gitlab::VisibilityLevel::PUBLIC visibility_level: Gitlab::VisibilityLevel::PUBLIC
} }
...@@ -14,7 +15,10 @@ describe Projects::CreateFromTemplateService do ...@@ -14,7 +15,10 @@ describe Projects::CreateFromTemplateService do
subject { described_class.new(user, project_params) } subject { described_class.new(user, project_params) }
it 'calls the importer service' do it 'calls the importer service' do
expect_any_instance_of(Projects::GitlabProjectsImportService).to receive(:execute) import_service_double = double
allow(Projects::GitlabProjectsImportService).to receive(:new).and_return(import_service_double)
expect(import_service_double).to receive(:execute)
subject.execute subject.execute
end end
...@@ -26,6 +30,31 @@ describe Projects::CreateFromTemplateService do ...@@ -26,6 +30,31 @@ describe Projects::CreateFromTemplateService do
expect(project.import_scheduled?).to be(true) expect(project.import_scheduled?).to be(true)
end end
context 'when template is not present' do
let(:template_name) { 'non_existent' }
let(:project) { subject.execute }
before do
expect(project).to be_saved
end
it 'does not set import set import type' do
expect(project.import_type).to be nil
end
it 'does not set import set import source' do
expect(project.import_source).to be nil
end
it 'is not scheduled' do
expect(project.import_scheduled?).to be(false)
end
it 'repository is empty' do
expect(project.repository.empty?).to be(true)
end
end
context 'the result project' do context 'the result project' do
before do before do
perform_enqueued_jobs do perform_enqueued_jobs do
......
...@@ -6,60 +6,10 @@ describe Projects::GitlabProjectsImportService do ...@@ -6,60 +6,10 @@ describe Projects::GitlabProjectsImportService do
let(:file) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') } let(:file) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
let(:overwrite) { false } let(:overwrite) { false }
let(:import_params) { { namespace_id: namespace.id, path: path, file: file, overwrite: overwrite } } let(:import_params) { { namespace_id: namespace.id, path: path, file: file, overwrite: overwrite } }
subject { described_class.new(namespace.owner, import_params) } subject { described_class.new(namespace.owner, import_params) }
describe '#execute' do describe '#execute' do
context 'with an invalid path' do it_behaves_like 'gitlab projects import validations'
let(:path) { '/invalid-path/' }
it 'returns an invalid project' do
project = subject.execute
expect(project).not_to be_persisted
expect(project).not_to be_valid
end
end
context 'with a valid path' do
it 'creates a project' do
project = subject.execute
expect(project).to be_persisted
expect(project).to be_valid
end
end
context 'override params' do
it 'stores them as import data when passed' do
project = described_class
.new(namespace.owner, import_params, description: 'Hello')
.execute
expect(project.import_data.data['override_params']['description']).to eq('Hello')
end
end
context 'when there is a project with the same path' do
let(:existing_project) { create(:project, namespace: namespace) }
let(:path) { existing_project.path}
it 'does not create the project' do
project = subject.execute
expect(project).to be_invalid
expect(project).not_to be_persisted
end
context 'when overwrite param is set' do
let(:overwrite) { true }
it 'creates a project in a temporary full_path' do
project = subject.execute
expect(project).to be_valid
expect(project).to be_persisted
end
end
end
end end
end end
shared_examples 'gitlab projects import validations' do
context 'with an invalid path' do
let(:path) { '/invalid-path/' }
it 'returns an invalid project' do
project = subject.execute
expect(project).not_to be_persisted
expect(project).not_to be_valid
end
end
context 'with a valid path' do
it 'creates a project' do
project = subject.execute
expect(project).to be_persisted
expect(project).to be_valid
end
end
context 'override params' do
it 'stores them as import data when passed' do
project = described_class
.new(namespace.owner, import_params, description: 'Hello')
.execute
expect(project.import_data.data['override_params']['description']).to eq('Hello')
end
end
context 'when there is a project with the same path' do
let(:existing_project) { create(:project, namespace: namespace) }
let(:path) { existing_project.path}
it 'does not create the project' do
project = subject.execute
expect(project).to be_invalid
expect(project).not_to be_persisted
end
context 'when overwrite param is set' do
let(:overwrite) { true }
it 'creates a project in a temporary full_path' do
project = subject.execute
expect(project).to be_valid
expect(project).to be_persisted
end
end
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