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,39 +15,17 @@ module Projects ...@@ -12,39 +15,17 @@ 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)
FileUtils.copy_entry(file.path, import_upload_path)
@overwrite = params.delete(:overwrite)
data = {}
data[:override_params] = @override_params if @override_params
if overwrite_project?
data[:original_path] = params[:path]
params[:path] += "-#{tmp_filename}"
end
params[:import_type] = 'gitlab_project' prepare_import_params
params[:import_source] = import_upload_path
params[:import_data] = { data: data } if data.present?
::Projects::CreateService.new(current_user, params).execute ::Projects::CreateService.new(current_user, params).execute
end end
private private
def import_upload_path
@import_upload_path ||= Gitlab::ImportExport.import_upload_path(filename: tmp_filename)
end
def tmp_filename
SecureRandom.hex
end
def overwrite_project? def overwrite_project?
@overwrite && project_with_same_full_path? overwrite? && project_with_same_full_path?
end end
def project_with_same_full_path? def project_with_same_full_path?
...@@ -52,7 +33,38 @@ module Projects ...@@ -52,7 +33,38 @@ module Projects
end end
def current_namespace def current_namespace
@current_namespace ||= Namespace.find_by(id: params[:namespace_id]) strong_memoize(:current_namespace) do
Namespace.find_by(id: params[:namespace_id])
end
end
def overwrite?
strong_memoize(:overwrite) do
params.delete(:overwrite)
end
end
def template_file
strong_memoize(:template_file) do
params.delete(:file)
end
end
def prepare_import_params
data = {}
data[:override_params] = @override_params if @override_params
if overwrite_project?
data[:original_path] = params[:path]
params[:path] += "-#{tmp_filename}"
end
if template_file
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
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
When you create a new repo locally, instead of going to GitLab to manually When you create a new repo locally, instead of going to GitLab to manually
create a new project and then push the repo, you can directly push it to create a new project and then push the repo, you can directly push it to
GitLab to create the new project, all without leaving your terminal. If you have access to that GitLab to create the new project, all without leaving your terminal. If you have access to that
namespace, we will automatically create a new project under that GitLab namespace with its namespace, we will automatically create a new project under that GitLab namespace with its
visibility set to Private by default (you can later change it in the [project's settings](../public_access/public_access.md#how-to-change-project-visibility)). visibility set to Private by default (you can later change it in the [project's settings](../public_access/public_access.md#how-to-change-project-visibility)).
This can be done by using either SSH or HTTP: This can be done by using either SSH or HTTP:
......
...@@ -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