Implement instance level project templates

parent 0399de90
...@@ -2,6 +2,7 @@ class UsersController < ApplicationController ...@@ -2,6 +2,7 @@ class UsersController < ApplicationController
include RoutableActions include RoutableActions
include RendersMemberAccess include RendersMemberAccess
include ControllerWithCrossProjectAccessCheck include ControllerWithCrossProjectAccessCheck
prepend EE::UsersController
requires_cross_project_access show: false, requires_cross_project_access show: false,
groups: false, groups: false,
......
...@@ -2,21 +2,28 @@ ...@@ -2,21 +2,28 @@
module Projects module Projects
class CreateFromTemplateService < BaseService class CreateFromTemplateService < BaseService
prepend ::EE::Projects::CreateFromTemplateService
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,10 @@ ...@@ -5,6 +5,10 @@
# 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
prepend ::EE::Projects::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 +16,17 @@ module Projects ...@@ -12,39 +16,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 +34,38 @@ module Projects ...@@ -52,7 +34,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
...@@ -5,11 +5,12 @@ class RepositoryImportWorker ...@@ -5,11 +5,12 @@ class RepositoryImportWorker
include ExceptionBacktrace include ExceptionBacktrace
include ProjectStartImport include ProjectStartImport
include ProjectImportOptions include ProjectImportOptions
prepend EE::RepositoryImportWorker
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,28 +22,30 @@ class RepositoryImportWorker ...@@ -21,28 +22,30 @@ 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
project.after_import project.after_import
# Explicitly enqueue mirror for update so
# that upstream remote is created and fetched
project.force_import_job! if project.mirror?
end end
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
...@@ -63,6 +63,10 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d ...@@ -63,6 +63,10 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
get :exists get :exists
get :pipelines_quota get :pipelines_quota
get '/', to: redirect('%{username}'), as: nil get '/', to: redirect('%{username}'), as: nil
## EE-specific
get :available_templates, format: :json
## EE-specific
end end
# Compatibility with old routing # Compatibility with old routing
......
...@@ -208,6 +208,7 @@ ActiveRecord::Schema.define(version: 20180722103201) do ...@@ -208,6 +208,7 @@ ActiveRecord::Schema.define(version: 20180722103201) do
t.boolean "enforce_terms", default: false t.boolean "enforce_terms", default: false
t.boolean "pseudonymizer_enabled", default: false, null: false t.boolean "pseudonymizer_enabled", default: false, null: false
t.boolean "hide_third_party_offers", default: false, null: false t.boolean "hide_third_party_offers", default: false, null: false
t.integer "custom_project_templates_group_id"
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
...@@ -2836,6 +2837,7 @@ ActiveRecord::Schema.define(version: 20180722103201) do ...@@ -2836,6 +2837,7 @@ ActiveRecord::Schema.define(version: 20180722103201) do
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree
add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify
add_foreign_key "approvals", "merge_requests", name: "fk_310d714958", on_delete: :cascade add_foreign_key "approvals", "merge_requests", name: "fk_310d714958", on_delete: :cascade
add_foreign_key "approver_groups", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "approver_groups", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade
......
...@@ -124,6 +124,7 @@ created in snippets, wikis, and repos. ...@@ -124,6 +124,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](../user/admin_area/custom_project_templates.md): 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:
......
## Custom project templates **[PREMIUM ONLY]**
> **Notes:**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6860) in [GitLab Premium](https://about.gitlab.com/pricing) 11.2
When you create a new project, creating it based on custom project templates is
a convenient option to bootstrap from an existing project boilerplate.
The administration setting to configure a GitLab group that serves as template
source can be found under **Admin > Settings > Custom project templates**.
Within this section, you can configure the group where all the custom project
templates are sourced. Every project directly under the group namespace will be
available to the user if they have access to them. For example: Every public
project in the group will be available to every logged user. However,
private projects will be available only if the user has view [permissions](../permissions.md)
in the project:
- Project Owner, Maintainer, Developer, Reporter or Guest
- Is a member of the Group: Owner, Maintainer, Developer, Reporter or Guest
Projects below subgroups of the template group are **not** supported.
Repository and database information that are copied over to each new project are
identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md).
...@@ -16,6 +16,10 @@ module EE ...@@ -16,6 +16,10 @@ module EE
attrs += EE::ApplicationSettingsHelper.external_authorization_service_attributes attrs += EE::ApplicationSettingsHelper.external_authorization_service_attributes
end end
if License.feature_available?(:custom_project_templates)
attrs << :custom_project_templates_group_id
end
if License.feature_available?(:email_additional_text) if License.feature_available?(:email_additional_text)
attrs << :email_additional_text attrs << :email_additional_text
end end
......
...@@ -21,6 +21,7 @@ module EE ...@@ -21,6 +21,7 @@ module EE
service_desk_enabled service_desk_enabled
external_authorization_classification_label external_authorization_classification_label
ci_cd_only ci_cd_only
use_custom_template
] ]
if allow_mirror_params? if allow_mirror_params?
......
module EE
module UsersController
def available_templates
render json: load_custom_project_templates
end
private
def load_custom_project_templates
@custom_project_templates ||= user.available_custom_project_templates(search: params[:search]).page(params[:page])
end
end
end
...@@ -101,7 +101,8 @@ module EE ...@@ -101,7 +101,8 @@ module EE
slack_app_enabled: false, slack_app_enabled: false,
slack_app_id: nil, slack_app_id: nil,
slack_app_secret: nil, slack_app_secret: nil,
slack_app_verification_token: nil slack_app_verification_token: nil,
custom_project_templates_group_id: nil
) )
end end
end end
...@@ -168,6 +169,20 @@ module EE ...@@ -168,6 +169,20 @@ module EE
alias_method :external_authorization_service_enabled?, alias_method :external_authorization_service_enabled?,
:external_authorization_service_enabled :external_authorization_service_enabled
def custom_project_templates_enabled?
License.feature_available?(:custom_project_templates)
end
def custom_project_templates_group_id
custom_project_templates_enabled? && super
end
def available_custom_project_templates
return [] unless group_id = custom_project_templates_group_id
::Project.where(namespace_id: group_id)
end
private private
def mirror_max_delay_in_minutes def mirror_max_delay_in_minutes
......
...@@ -290,7 +290,10 @@ module EE ...@@ -290,7 +290,10 @@ module EE
UpdateAllMirrorsWorker.perform_async UpdateAllMirrorsWorker.perform_async
end end
override :add_import_job
def add_import_job def add_import_job
return if gitlab_custom_project_template_import?
if import? && !repository_exists? if import? && !repository_exists?
super super
elsif mirror? elsif mirror?
...@@ -519,6 +522,16 @@ module EE ...@@ -519,6 +522,16 @@ module EE
log_geo_events log_geo_events
end end
override :import?
def import?
super || gitlab_custom_project_template_import?
end
def gitlab_custom_project_template_import?
import_type == 'gitlab_custom_project_template' &&
::Gitlab::CurrentSettings.custom_project_templates_enabled?
end
private private
def set_override_pull_mirror_available def set_override_pull_mirror_available
......
...@@ -93,5 +93,14 @@ module EE ...@@ -93,5 +93,14 @@ module EE
def email_opted_in_source def email_opted_in_source
email_opted_in_source_id == EMAIL_OPT_IN_SOURCE_ID_GITLAB_COM ? 'GitLab.com' : '' email_opted_in_source_id == EMAIL_OPT_IN_SOURCE_ID_GITLAB_COM ? 'GitLab.com' : ''
end end
def available_custom_project_templates(search: nil)
templates = ::Gitlab::CurrentSettings.available_custom_project_templates
ProjectsFinder.new(current_user: self,
project_ids_relation: templates,
params: { search: search, sort: 'name_asc' })
.execute
end
end end
end end
...@@ -61,6 +61,7 @@ class License < ActiveRecord::Base ...@@ -61,6 +61,7 @@ class License < ActiveRecord::Base
external_authorization_service external_authorization_service
ci_cd_projects ci_cd_projects
system_header_footer system_header_footer
custom_project_templates
].freeze ].freeze
EEU_FEATURES = EEP_FEATURES + %i[ EEU_FEATURES = EEP_FEATURES + %i[
...@@ -157,6 +158,7 @@ class License < ActiveRecord::Base ...@@ -157,6 +158,7 @@ class License < ActiveRecord::Base
repository_size_limit repository_size_limit
external_authorization_service external_authorization_service
system_header_footer system_header_footer
custom_project_templates
].freeze ].freeze
validate :valid_license validate :valid_license
......
module EE
module Projects
module CreateFromTemplateService
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
override :execute
def execute
return super unless use_custom_template?
override_params = params.dup
params[:custom_template] = template_project if template_project
::Projects::GitlabProjectsImportService.new(current_user, params, override_params).execute
end
private
def use_custom_template?
strong_memoize(:use_custom_template) do
template_name &&
::Gitlab::Utils.to_boolean(params.delete(:use_custom_template)) &&
::Gitlab::CurrentSettings.custom_project_templates_enabled?
end
end
def template_project
strong_memoize(:template_project) do
current_user.available_custom_project_templates(search: template_name).first
end
end
end
end
end
module EE
module Projects
module GitlabProjectsImportService
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
override :execute
def execute
super.tap do |project|
if project.saved? && custom_template
custom_template.add_export_job(current_user: current_user,
after_export_strategy: export_strategy(project))
end
end
end
private
override :prepare_import_params
def prepare_import_params
super
if custom_template
params[:import_type] = 'gitlab_custom_project_template'
params[:import_source] = custom_template.id
end
end
def custom_template
strong_memoize(:custom_template) do
params.delete(:custom_template)
end
end
def export_strategy(project)
Gitlab::ImportExport::AfterExportStrategies::CustomTemplateExportImportStrategy.new(export_into_project_id: project.id)
end
end
end
end
module EE
module RepositoryImportWorker
extend ::Gitlab::Utils::Override
override :perform
def perform(project_id)
super
# Explicitly enqueue mirror for update so
# that upstream remote is created and fetched
project.force_import_job! if project.mirror?
end
override :template_import?
def template_import?
super || project.gitlab_custom_project_template_import?
end
end
end
---
title: Implement custom project templates
merge_request: 6436
author:
type: added
class AddCustomProjectTemplatesGroupIdToApplicationSettings < ActiveRecord::Migration
DOWNTIME = false
def up
add_column(:application_settings, :custom_project_templates_group_id, :integer)
add_foreign_key(:application_settings, :namespaces, column: :custom_project_templates_group_id, on_delete: :nullify) # rubocop: disable Migration/AddConcurrentForeignKey
end
def down
remove_foreign_key(:application_settings, column: :custom_project_templates_group_id)
remove_column(:application_settings, :custom_project_templates_group_id)
end
end
module EE
module Gitlab
module ImportExport
module AfterExportStrategies
class CustomTemplateExportImportStrategy < ::Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy
include ::Gitlab::Utils::StrongMemoize
include ::Gitlab::TemplateHelper
validates :export_into_project_id, presence: true
def initialize(export_into_project_id:)
super
end
protected
def strategy_execute
return unless export_into_project_exists?
prepare_template_environment(export_file_path)
set_import_attributes
::RepositoryImportWorker.new.perform(export_into_project_id)
ensure
project.remove_exported_project_file
end
def export_file_path
strong_memoize(:export_file_path) do
if object_storage?
project.import_export_upload.export_file.path
else
project.export_project_path
end
end
end
def set_import_attributes
::Project.update(export_into_project_id, import_source: import_upload_path)
end
def export_into_project_exists?
::Project.exists?(export_into_project_id)
end
end
end
end
end
end
module EE
module Gitlab
module ImportSources
extend ::Gitlab::Utils::Override
override :import_table
def import_table
super + ee_import_table
end
def ee_import_table
# This method can be called/loaded before the database
# has been created. With this guard clause we prevent calling
# the License table until the connection has been established
return [] unless ActiveRecord::Base.connected? && License.feature_available?(:custom_project_templates)
[::Gitlab::ImportSources::ImportSource.new('gitlab_custom_project_template',
'GitLab custom project template export',
::Gitlab::ImportExport::Importer)]
end
end
end
end
...@@ -104,6 +104,14 @@ describe Admin::ApplicationSettingsController do ...@@ -104,6 +104,14 @@ describe Admin::ApplicationSettingsController do
it_behaves_like 'settings for licensed features' it_behaves_like 'settings for licensed features'
end end
context 'custom project templates settings' do
let(:group) { create(:group) }
let(:settings) { { custom_project_templates_group_id: group.id } }
let(:feature) { :custom_project_templates }
it_behaves_like 'settings for licensed features'
end
it 'updates the default_project_creation for string value' do it 'updates the default_project_creation for string value' do
stub_licensed_features(project_creation_level: true) stub_licensed_features(project_creation_level: true)
put :update, application_setting: { default_project_creation: ::EE::Gitlab::Access::MAINTAINER_PROJECT_ACCESS } put :update, application_setting: { default_project_creation: ::EE::Gitlab::Access::MAINTAINER_PROJECT_ACCESS }
......
...@@ -51,6 +51,49 @@ describe ProjectsController do ...@@ -51,6 +51,49 @@ describe ProjectsController do
expect(created_project.reload.mirror_user).to be nil expect(created_project.reload.mirror_user).to be nil
end end
end end
context 'custom project templates' do
let(:group) { create(:group) }
let(:project_template) { create(:project, :repository, :public, namespace: group) }
let(:templates_params) do
{
path: 'foo',
description: 'bar',
namespace_id: user.namespace.id,
use_custom_template: true,
template_name: project_template.name
}
end
context 'when licensed' do
before do
stub_licensed_features(custom_project_templates: true)
stub_ee_application_setting(custom_project_templates_group_id: group.id)
end
it 'creates the project from project template' do
post :create, project: templates_params
created_project = Project.find_by_path('foo')
expect(flash[:notice]).to eq "Project 'foo' was successfully created."
expect(created_project.repository.empty?).to be false
end
end
context 'when unlicensed' do
before do
stub_licensed_features(custom_project_templates: false)
end
it 'creates the project from project template' do
post :create, project: templates_params
created_project = Project.find_by_path('foo')
expect(flash[:notice]).to eq "Project 'foo' was successfully created."
expect(created_project.repository.empty?).to be true
end
end
end
end end
describe 'PUT #update' do describe 'PUT #update' do
...@@ -177,33 +220,6 @@ describe ProjectsController do ...@@ -177,33 +220,6 @@ describe ProjectsController do
end end
end end
context 'external authaurization service attributes' do
def update_classification_label
put :update,
namespace_id: project.namespace,
id: project,
project: { external_authorization_classification_label: 'new_label' }
project.reload
end
it 'updates the project classification label' do
external_service_allow_access(user, project)
expect(EE::Gitlab::ExternalAuthorization)
.to receive(:access_allowed?).with(user, 'new_label') { true }
expect { update_classification_label }
.to change(project, :external_authorization_classification_label).to('new_label')
end
it 'does not update the project classification label when the feature is not available' do
stub_licensed_features(external_authorization_service: false)
expect { update_classification_label }
.not_to change(project, :external_authorization_classification_label)
end
end
it_behaves_like 'unauthorized when external service denies access' do it_behaves_like 'unauthorized when external service denies access' do
subject do subject do
put :update, put :update,
......
require 'spec_helper'
describe EE::Gitlab::ImportExport::AfterExportStrategies::CustomTemplateExportImportStrategy do
let!(:project_template) { create(:project, :repository, :with_export) }
let(:project) { create(:project, :import_scheduled, import_type: 'gitlab_custom_project_template') }
let(:user) { build(:user) }
let(:repository_import_worker) { RepositoryImportWorker.new }
subject { described_class.new(export_into_project_id: project.id) }
before do
stub_licensed_features(custom_project_templates: true)
allow(RepositoryImportWorker).to receive(:new).and_return(repository_import_worker)
allow(repository_import_worker).to receive(:perform)
end
describe 'validations' do
it 'export_into_project_id must be present' do
expect(described_class.new(export_into_project_id: nil)).to be_invalid
expect(described_class.new(export_into_project_id: 1)).to be_valid
end
end
describe '#execute' do
it 'updates the project import_source with the path to import' do
allow(subject).to receive(:import_upload_path).and_return('path')
expect(Project).to receive(:update).with(project.id, import_source: 'path').and_call_original
subject.execute(user, project_template)
expect(project.reload.import_source).to eq 'path'
end
it 'imports repository' do
expect(repository_import_worker).to receive(:perform).with(project.id).and_call_original
subject.execute(user, project_template)
expect(project_template.repository.ls_files('HEAD')).to eq project.repository.ls_files('HEAD')
end
it 'removes the exported project file after the import' do
expect(project_template).to receive(:remove_exported_project_file)
subject.execute(user, project_template)
end
describe 'export_file_path' do
before do
allow(subject).to receive(:project).and_return(project_template)
end
after do
subject.send(:export_file_path)
end
context 'without object storage' do
it 'returns the local path' do
expect(project_template).to receive(:export_project_path)
end
end
context 'with object storage' do
let(:project_template) { create(:project, :with_object_export) }
it 'returns the path from object storage' do
expect(project_template.import_export_upload.export_file).to receive(:path)
end
end
end
end
end
require 'spec_helper'
describe Gitlab::ImportSources do
describe '.import_table' do
it 'includes specific EE imports types when the license supports them' do
stub_licensed_features(custom_project_templates: true)
expect(described_class.ee_import_table).not_to be_empty
expect(described_class.import_table).to include(*described_class.ee_import_table)
end
end
end
...@@ -23,6 +23,7 @@ describe ApplicationSetting do ...@@ -23,6 +23,7 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(1.1).for(:mirror_capacity_threshold) } it { is_expected.not_to allow_value(1.1).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(-1).for(:mirror_capacity_threshold) } it { is_expected.not_to allow_value(-1).for(:mirror_capacity_threshold) }
it { is_expected.not_to allow_value(subject.mirror_max_capacity + 1).for(:mirror_capacity_threshold) } it { is_expected.not_to allow_value(subject.mirror_max_capacity + 1).for(:mirror_capacity_threshold) }
it { is_expected.to allow_value(nil).for(:custom_project_templates_group_id) }
describe 'when additional email text is enabled' do describe 'when additional email text is enabled' do
before do before do
...@@ -209,4 +210,69 @@ describe ApplicationSetting do ...@@ -209,4 +210,69 @@ describe ApplicationSetting do
) )
end end
end end
describe 'custom project templates' do
let(:group) { create(:group) }
let(:projects) { create_list(:project, 3, namespace: group) }
before do
setting.update_column(:custom_project_templates_group_id, group.id)
setting.reload
end
context 'when custom_project_templates feature is enabled' do
before do
stub_licensed_features(custom_project_templates: true)
end
describe '#custom_project_templates_enabled?' do
it 'returns true' do
expect(setting.custom_project_templates_enabled?).to be_truthy
end
end
describe '#custom_project_template_id' do
it 'returns group id' do
expect(setting.custom_project_templates_group_id).to eq group.id
end
end
describe '#available_custom_project_templates' do
it 'returns group projects' do
expect(setting.available_custom_project_templates).to match_array(projects)
end
it 'returns an empty array if group is not set' do
allow(setting).to receive(:custom_project_template_id).and_return(nil)
expect(setting.available_custom_project_templates).to eq []
end
end
end
context 'when custom_project_templates feature is disabled' do
before do
stub_licensed_features(custom_project_templates: false)
end
describe '#custom_project_templates_enabled?' do
it 'returns false' do
expect(setting.custom_project_templates_enabled?).to be false
end
end
describe '#custom_project_template_id' do
it 'returns false' do
expect(setting.custom_project_templates_group_id).to be false
end
end
describe '#available_custom_project_templates' do
it 'returns an empty relation' do
expect(setting.available_custom_project_templates).to be_empty
end
end
end
end
end end
...@@ -158,4 +158,64 @@ describe EE::User do ...@@ -158,4 +158,64 @@ describe EE::User do
end end
end end
end end
describe '#available_custom_project_templates' do
let(:user) { create(:user) }
it 'returns an empty relation if group is not set' do
expect(user.available_custom_project_templates.empty?).to be_truthy
end
context 'when group with custom project templates is set' do
let(:group) { create(:group) }
before do
stub_ee_application_setting(custom_project_templates_group_id: group.id)
end
it 'returns an empty relation if group has no available project templates' do
expect(group.projects.empty?).to be true
expect(user.available_custom_project_templates.empty?).to be true
end
context 'when group has custom project templates' do
let!(:private_project) { create :project, :private, namespace: group, name: 'private_project' }
let!(:internal_project) { create :project, :internal, namespace: group, name: 'internal_project' }
let!(:public_project) { create :project, :public, namespace: group, name: 'public_project' }
it 'returns public projects' do
expect(user.available_custom_project_templates).to include public_project
end
context 'returns private projects if user' do
it 'is a member of the project' do
expect(user.available_custom_project_templates).not_to include private_project
private_project.add_developer(user)
expect(user.available_custom_project_templates).to include private_project
end
it 'is a member of the group' do
expect(user.available_custom_project_templates).not_to include private_project
group.add_developer(user)
expect(user.available_custom_project_templates).to include private_project
end
end
it 'returns internal projects' do
expect(user.available_custom_project_templates).to include internal_project
end
it 'allows to search available project templates by name' do
projects = user.available_custom_project_templates(search: 'publi')
expect(projects.count).to eq 1
expect(projects.first).to eq public_project
end
end
end
end
end end
...@@ -1329,6 +1329,35 @@ describe Project do ...@@ -1329,6 +1329,35 @@ describe Project do
end end
end end
describe 'Project import job' do
let(:project) { create(:project, import_url: generate(:url)) }
before do
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository)
.with(project.repository_storage, project.disk_path, project.import_url)
.and_return(true)
# Works around https://github.com/rspec/rspec-mocks/issues/910
allow(described_class).to receive(:find).with(project.id).and_return(project)
expect(project.repository).to receive(:after_import)
.and_call_original
expect(project.wiki.repository).to receive(:after_import)
.and_call_original
end
context 'with a mirrored project' do
let(:project) { create(:project, :mirror) }
it 'calls RepositoryImportWorker and inserts in front of the mirror scheduler queue' do
allow_any_instance_of(described_class).to receive(:repository_exists?).and_return(false, true)
expect_any_instance_of(EE::Project).to receive(:force_import_job!)
expect(RepositoryImportWorker).to receive(:perform_async).with(project.id).and_call_original
expect { project.import_schedule }.to change { project.import_jid }
end
end
end
describe '#licensed_features' do describe '#licensed_features' do
let(:plan_license) { :free_plan } let(:plan_license) { :free_plan }
let(:global_license) { create(:license) } let(:global_license) { create(:license) }
...@@ -1535,4 +1564,36 @@ describe Project do ...@@ -1535,4 +1564,36 @@ describe Project do
project.after_import project.after_import
end end
end end
describe '#add_import_job' do
let(:project) { create(:project) }
context 'when import_type is gitlab_custom_project_template_import' do
it 'does not create import job' do
project.import_type = 'gitlab_custom_project_template_import'
expect(project.add_import_job).to be_nil
end
end
end
describe '#gitlab_custom_project_template_import?' do
let(:project) { create(:project, import_type: 'gitlab_custom_project_template') }
context 'when licensed' do
before do
stub_licensed_features(custom_project_templates: true)
end
it 'returns true' do
expect(project.gitlab_custom_project_template_import?).to be true
end
end
context 'when unlicensed' do
it 'returns false' do
expect(project.gitlab_custom_project_template_import?).to be false
end
end
end
end end
require 'spec_helper'
describe Projects::CreateFromTemplateService do
let(:group) { create(:group) }
let(:project) { create(:project, :public, namespace: group) }
let(:user) { create(:user) }
let(:project_name) { project.name }
let(:use_custom_template) { true }
let(:project_params) do
{
path: user.to_param,
template_name: project_name,
description: 'project description',
visibility_level: Gitlab::VisibilityLevel::PUBLIC,
use_custom_template: use_custom_template
}
end
subject { described_class.new(user, project_params) }
before do
stub_licensed_features(custom_project_templates: true)
stub_ee_application_setting(custom_project_templates_group_id: group.id)
end
context '#execute' do
context 'does not create project from custom template' do
after do
project = subject.execute
expect(project).to be_saved
expect(project.repository.empty?).to be true
end
context 'when use_custom_template is not present or false' do
let(:use_custom_template) { false }
it 'creates an empty project' do
expect(::Gitlab::ProjectTemplate).to receive(:find)
expect(subject).not_to receive(:find_template_project)
end
end
context 'when custom_project_templates feature is not enabled' do
it 'creates an empty project' do
stub_licensed_features(custom_project_templates: false)
expect(::Gitlab::ProjectTemplate).to receive(:find)
expect(subject).not_to receive(:find_template_project)
end
end
context 'when custom_project_template does not exist' do
let(:project_name) { 'whatever' }
it 'creates an empty project' do
expect(::Projects::GitlabProjectsImportService)
.to receive(:new).with(user, hash_excluding(:custom_template), anything).and_call_original
end
end
end
context 'creates project from custom template' do
# If we move the project inside a let block it throws a SEGFAULT error
before do
@project = subject.execute
end
it 'returns the created project' do
expect(@project).to be_saved
expect(@project.import_scheduled?).to be(true)
end
context 'the result project' do
it 'overrides template description' do
expect(@project.description).to match('project description')
end
it 'overrides template visibility_level' do
expect(@project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
end
end
end
end
require 'spec_helper'
describe Projects::GitlabProjectsImportService do
set(:namespace) { create(:namespace) }
let(:path) { 'test-path' }
let(:custom_template) { create(:project) }
let(:overwrite) { false }
let(:import_params) { { namespace_id: namespace.id, path: path, custom_template: custom_template, overwrite: overwrite } }
subject { described_class.new(namespace.owner, import_params) }
after do
TestEnv.clean_test_path
end
describe '#execute' do
context 'creates export job' do
it 'if project saved and custom template exists' do
expect(custom_template).to receive(:add_export_job)
project = subject.execute
expect(project.saved?).to be true
end
it 'sets custom template import strategy after export' do
expect(custom_template)
.to receive(:add_export_job).with(current_user: namespace.owner,
after_export_strategy: instance_of(EE::Gitlab::ImportExport::AfterExportStrategies::CustomTemplateExportImportStrategy))
subject.execute
end
end
context 'does not create export job' do
it 'if project not saved' do
allow_any_instance_of(Project).to receive(:saved?).and_return(false)
expect(custom_template).not_to receive(:add_export_job)
project = subject.execute
expect(project.saved?).to be false
end
end
it_behaves_like 'gitlab projects import validations'
end
end
require 'spec_helper'
describe RepositoryImportWorker do
let(:project) { create(:project, :import_scheduled) }
it 'updates the error on custom project template Import/Export' do
stub_licensed_features(custom_project_templates: true)
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
project.update(import_jid: '123', import_type: 'gitlab_custom_project_template')
expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
expect do
subject.perform(project.id)
end.to raise_error(RuntimeError, error)
expect(project.reload.import_error).not_to be_nil
end
context 'when project is a mirror' do
let(:project) { create(:project, :mirror, :import_scheduled) }
it 'adds mirror in front of the mirror scheduler queue' do
expect_any_instance_of(Projects::ImportService).to receive(:execute)
.and_return({ status: :ok })
expect_any_instance_of(EE::Project).to receive(:force_import_job!)
subject.perform(project.id)
end
end
end
...@@ -29,7 +29,7 @@ describe UpdateAllMirrorsWorker do ...@@ -29,7 +29,7 @@ describe UpdateAllMirrorsWorker do
def schedule_mirrors!(capacity:) def schedule_mirrors!(capacity:)
allow(Gitlab::Mirror).to receive_messages(available_capacity: capacity) allow(Gitlab::Mirror).to receive_messages(available_capacity: capacity)
allow_any_instance_of(RepositoryImportWorker).to receive(:perform) allow(RepositoryImportWorker).to receive(:perform_async)
Sidekiq::Testing.inline! do Sidekiq::Testing.inline! do
worker.schedule_mirrors! worker.schedule_mirrors!
......
...@@ -21,25 +21,31 @@ module Gitlab ...@@ -21,25 +21,31 @@ module Gitlab
].freeze ].freeze
class << self class << self
prepend EE::Gitlab::ImportSources
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
...@@ -1912,23 +1912,11 @@ describe Project do ...@@ -1912,23 +1912,11 @@ describe Project do
end end
it 'imports a project' do it 'imports a project' do
expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original expect(RepositoryImportWorker).to receive(:perform_async).and_call_original
expect { project.import_schedule }.to change { project.import_jid } expect { project.import_schedule }.to change { project.import_jid }
expect(project.reload.import_status).to eq('finished') expect(project.reload.import_status).to eq('finished')
end end
context 'with a mirrored project' do
let(:project) { create(:project, :mirror) }
it 'calls RepositoryImportWorker and inserts in front of the mirror scheduler queue' do
allow_any_instance_of(described_class).to receive(:repository_exists?).and_return(false, true)
expect_any_instance_of(EE::Project).to receive(:force_import_job!)
expect_any_instance_of(RepositoryImportWorker).to receive(:perform).with(project.id).and_call_original
expect { project.import_schedule }.to change { project.import_jid }
end
end
end end
describe 'project import state transitions' do describe 'project import state transitions' do
......
...@@ -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
...@@ -47,19 +47,6 @@ describe RepositoryImportWorker do ...@@ -47,19 +47,6 @@ describe RepositoryImportWorker do
end end
end end
context 'when project is a mirror' do
let(:project) { create(:project, :mirror, :import_scheduled) }
it 'adds mirror in front of the mirror scheduler queue' do
expect_any_instance_of(Projects::ImportService).to receive(:execute)
.and_return({ status: :ok })
expect_any_instance_of(EE::Project).to receive(:force_import_job!)
subject.perform(project.id)
end
end
context 'when the import has failed' do context 'when the import has failed' do
it 'hide the credentials that were used in the import URL' do it 'hide the credentials that were used in the import URL' do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
......
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