Commit c6cd689f authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch 'sh-create-project-from-template-api' into 'master'

Support creating project from template via API

See merge request gitlab-org/gitlab!16352
parents 7a9d04f2 9b6dce93
...@@ -9,7 +9,9 @@ module Projects ...@@ -9,7 +9,9 @@ module Projects
end end
def execute def execute
file = Gitlab::ProjectTemplate.find(template_name)&.file return project unless validate_template!
file = built_in_template&.file
override_params = params.dup override_params = params.dup
params[:file] = file params[:file] = file
...@@ -24,6 +26,25 @@ module Projects ...@@ -24,6 +26,25 @@ module Projects
params.delete(:template_name).presence params.delete(:template_name).presence
end end
end end
private
def validate_template!
return true if built_in_template
project.errors.add(:template_name, _("'%{template_name}' is unknown or invalid" % { template_name: template_name }))
false
end
def built_in_template
strong_memoize(:built_in_template) do
Gitlab::ProjectTemplate.find(template_name)
end
end
def project
@project ||= ::Project.new(namespace_id: params[:namespace_id])
end
end end
end end
......
...@@ -991,6 +991,9 @@ POST /projects/user/:user_id ...@@ -991,6 +991,9 @@ POST /projects/user/:user_id
| `external_authorization_classification_label` | string | no | **(PREMIUM)** The classification label for the project | | `external_authorization_classification_label` | string | no | **(PREMIUM)** The classification label for the project |
| `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project | | `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project |
| `mirror_trigger_builds` | boolean | no | **(STARTER)** Pull mirroring triggers builds | | `mirror_trigger_builds` | boolean | no | **(STARTER)** Pull mirroring triggers builds |
| `template_name` | string | no | When used without `use_custom_template`, name of a [built-in project template](../gitlab-basics/create-project.md#built-in-templates). When used with `use_custom_template`, name of a custom project template |
| `use_custom_template` | boolean | no | **(PREMIUM)** Use either custom [instance](../user/admin_area/custom_project_templates.md) or [group](../user/group/custom_project_templates.md) (with `group_with_project_templates_id`) project template |
| `group_with_project_templates_id` | integer | no | **(PREMIUM)** For group-level custom templates, specifies ID of group from which all the custom project templates are sourced. Leave empty for instance-level templates. Requires `use_custom_template` to be true |
NOTE: **Note:** If your HTTP repository is not publicly accessible, NOTE: **Note:** If your HTTP repository is not publicly accessible,
add authentication information to the URL: `https://username:password@gitlab.company.com/group/project.git` add authentication information to the URL: `https://username:password@gitlab.company.com/group/project.git`
......
...@@ -9,11 +9,7 @@ module EE ...@@ -9,11 +9,7 @@ module EE
override :execute override :execute
def execute def execute
return super unless use_custom_template? return super unless use_custom_template?
return project unless validate_group_template!
if subgroup_id && !valid_project_namespace?
project.errors.add(:namespace, _("is not a descendant of the Group owning the template"))
return project
end
override_params = params.dup override_params = params.dup
params[:custom_template] = template_project if template_project params[:custom_template] = template_project if template_project
...@@ -23,6 +19,18 @@ module EE ...@@ -23,6 +19,18 @@ module EE
private private
def validate_group_template!
if subgroup_id && !valid_project_namespace?
project.errors.add(:namespace, _("is not a descendant of the Group owning the template"))
return false
end
return true if template_project.present?
project.errors.add(:template_name, _("'%{template_name}' is unknown or invalid" % { template_name: template_name }))
false
end
def use_custom_template? def use_custom_template?
strong_memoize(:use_custom_template) do strong_memoize(:use_custom_template) do
template_name && template_name &&
...@@ -51,10 +59,6 @@ module EE ...@@ -51,10 +59,6 @@ module EE
templates_owner.self_and_descendants.exists?(id: project.namespace_id) templates_owner.self_and_descendants.exists?(id: project.namespace_id)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def project
@project ||= ::Project.new(namespace_id: params[:namespace_id])
end
end end
end end
end end
---
title: Support creating project from template via API
merge_request: 16352
author:
type: added
...@@ -8,6 +8,13 @@ module EE ...@@ -8,6 +8,13 @@ module EE
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
prepended do prepended do
params :optional_create_project_params_ee do
optional :use_custom_template, type: Grape::API::Boolean, desc: "Use custom template"
given :use_custom_template do
optional :group_with_project_templates_id, type: Integer, desc: "Group ID that serves as the template source"
end
end
params :optional_project_params_ee do params :optional_project_params_ee do
optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins' optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins'
optional :approvals_before_merge, type: Integer, desc: 'How many approvers should approve merge request by default' optional :approvals_before_merge, type: Integer, desc: 'How many approvers should approve merge request by default'
......
...@@ -122,16 +122,19 @@ describe ProjectsController do ...@@ -122,16 +122,19 @@ describe ProjectsController do
end end
context 'when unlicensed' do context 'when unlicensed' do
render_views
before do before do
stub_licensed_features(custom_project_templates: false) stub_licensed_features(custom_project_templates: false)
project
project_template
end end
it 'creates the project from project template' do it 'does not create the project from project template' do
post :create, params: { project: templates_params } expect { post :create, params: { project: templates_params } }.not_to change { Project.count }
created_project = Project.find_by_path('foo') expect(response).to have_gitlab_http_status(200)
expect(flash[:notice]).to eq "Project 'foo' was successfully created." expect(response.body).to match(/Template name .* is unknown or invalid/)
expect(created_project.repository.empty?).to be true
end end
end end
end end
......
...@@ -183,6 +183,75 @@ describe API::Projects do ...@@ -183,6 +183,75 @@ describe API::Projects do
end end
describe 'POST /projects' do describe 'POST /projects' do
shared_examples 'creates projects with templates' do
before do
group.add_maintainer(user)
stub_licensed_features(custom_project_templates: true)
stub_ee_application_setting(custom_project_templates_group_id: group.id)
end
it 'creates a project using a template' do
expect(ProjectExportWorker).to receive(:perform_async).and_call_original
Sidekiq::Testing.fake! do
expect { post api('/projects', user), params: project_params }
.to change { Project.count }.by(1)
end
expect(response).to have_gitlab_http_status(201)
project = Project.find(json_response['id'])
expect(project.name).to eq(new_project_name)
end
it 'returns a 400 error for an invalid template name' do
project_params[:template_name] = 'bogus-template'
expect { post api('/projects', user), params: project_params }
.not_to change { Project.count }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['template_name']).to eq(["'bogus-template' is unknown or invalid"])
end
end
context 'with instance-level templates' do
let(:group) { create(:group) }
let!(:project) { create(:project, :public, namespace: group) }
let(:new_project_name) { "project-#{SecureRandom.hex}" }
let(:project_params) do
{
template_name: project.name,
name: new_project_name,
path: new_project_name,
use_custom_template: true,
namespace_id: group.id
}
end
it_behaves_like 'creates projects with templates'
end
context 'with group templates' do
let(:parent_group) { create(:group) }
let(:subgroup) { create(:group, :public, parent: parent_group) }
let(:group) { subgroup }
let!(:project) { create(:project, :public, namespace: subgroup) }
let(:new_project_name) { "project-#{SecureRandom.hex}" }
let(:project_params) do
{
template_name: project.name,
name: new_project_name,
path: new_project_name,
use_custom_template: true,
group_with_project_templates_id: subgroup.id,
namespace_id: subgroup.id
}
end
it_behaves_like 'creates projects with templates'
end
context 'when importing with mirror attributes' do context 'when importing with mirror attributes' do
let(:import_url) { generate(:url) } let(:import_url) { generate(:url) }
let(:mirror_params) do let(:mirror_params) do
......
...@@ -55,7 +55,7 @@ describe Projects::CreateFromTemplateService do ...@@ -55,7 +55,7 @@ describe Projects::CreateFromTemplateService do
after do after do
project = subject.execute project = subject.execute
expect(project).to be_saved expect(project).not_to be_saved
expect(project.repository.empty?).to be true expect(project.repository.empty?).to be true
end end
...@@ -80,9 +80,8 @@ describe Projects::CreateFromTemplateService do ...@@ -80,9 +80,8 @@ describe Projects::CreateFromTemplateService do
context 'when custom_project_template does not exist' do context 'when custom_project_template does not exist' do
let(:project_name) { 'whatever' } let(:project_name) { 'whatever' }
it 'creates an empty project' do it 'does not attempt to import a project' do
expect(::Projects::GitlabProjectsImportService) expect(::Projects::GitlabProjectsImportService).not_to receive(:new)
.to receive(:new).with(user, hash_excluding(:custom_template), anything).and_call_original
end end
end end
end end
......
...@@ -56,6 +56,14 @@ module API ...@@ -56,6 +56,14 @@ module API
use :optional_project_params_ee use :optional_project_params_ee
end end
params :optional_create_project_params_ee do
end
params :optional_create_project_params do
use :optional_project_params
use :optional_create_project_params_ee
end
params :optional_filter_params_ee do params :optional_filter_params_ee do
end end
......
...@@ -68,6 +68,8 @@ module API ...@@ -68,6 +68,8 @@ module API
params :create_params do params :create_params do
optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.' optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
optional :import_url, type: String, desc: 'URL from which the project is imported' optional :import_url, type: String, desc: 'URL from which the project is imported'
optional :template_name, type: String, desc: "Name of template from which to create project"
mutually_exclusive :import_url, :template_name
end end
def load_projects def load_projects
...@@ -155,7 +157,7 @@ module API ...@@ -155,7 +157,7 @@ module API
optional :name, type: String, desc: 'The name of the project' optional :name, type: String, desc: 'The name of the project'
optional :path, type: String, desc: 'The path of the repository' optional :path, type: String, desc: 'The path of the repository'
at_least_one_of :name, :path at_least_one_of :name, :path
use :optional_project_params use :optional_create_project_params
use :create_params use :create_params
end end
post do post do
......
...@@ -391,6 +391,9 @@ msgstr "" ...@@ -391,6 +391,9 @@ msgstr ""
msgid "'%{source}' is not a import source" msgid "'%{source}' is not a import source"
msgstr "" msgstr ""
msgid "'%{template_name}' is unknown or invalid"
msgstr ""
msgid "(%d closed)" msgid "(%d closed)"
msgid_plural "(%d closed)" msgid_plural "(%d closed)"
msgstr[0] "" msgstr[0] ""
......
...@@ -631,6 +631,33 @@ describe API::Projects do ...@@ -631,6 +631,33 @@ describe API::Projects do
expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED) expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
end end
it 'creates a project using a template' do
expect { post api('/projects', user), params: { template_name: 'rails', name: 'rails-test' } }
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(201)
project = Project.find(json_response['id'])
expect(project).to be_saved
expect(project.import_type).to eq('gitlab_project')
end
it 'returns 400 for an invalid template' do
expect { post api('/projects', user), params: { template_name: 'unknown', name: 'rails-test' } }
.not_to change { Project.count }
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['template_name']).to eq(["'unknown' is unknown or invalid"])
end
it 'disallows creating a project with an import_url and template' do
project_params = { import_url: 'http://example.com', template_name: 'rails', name: 'rails-test' }
expect { post api('/projects', user), params: project_params }
.not_to change { Project.count }
expect(response).to have_gitlab_http_status(400)
end
it 'sets a project as public' do it 'sets a project as public' do
project = attributes_for(:project, visibility: 'public') project = attributes_for(:project, visibility: 'public')
...@@ -867,7 +894,7 @@ describe API::Projects do ...@@ -867,7 +894,7 @@ describe API::Projects do
expect { post api("/projects/user/#{user.id}", admin), params: { name: 'Foo Project' } }.to change { Project.count }.by(1) expect { post api("/projects/user/#{user.id}", admin), params: { name: 'Foo Project' } }.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(201)
project = Project.last project = Project.find(json_response['id'])
expect(project.name).to eq('Foo Project') expect(project.name).to eq('Foo Project')
expect(project.path).to eq('foo-project') expect(project.path).to eq('foo-project')
...@@ -878,7 +905,7 @@ describe API::Projects do ...@@ -878,7 +905,7 @@ describe API::Projects do
.to change { Project.count }.by(1) .to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(201)
project = Project.last project = Project.find(json_response['id'])
expect(project.name).to eq('Foo Project') expect(project.name).to eq('Foo Project')
expect(project.path).to eq('path-project-Foo') expect(project.path).to eq('path-project-Foo')
......
...@@ -25,7 +25,7 @@ describe Projects::CreateFromTemplateService do ...@@ -25,7 +25,7 @@ describe Projects::CreateFromTemplateService do
subject.execute subject.execute
end end
it 'returns the project thats created' do it 'returns the project that is created' do
project = subject.execute project = subject.execute
expect(project).to be_saved expect(project).to be_saved
...@@ -37,7 +37,7 @@ describe Projects::CreateFromTemplateService do ...@@ -37,7 +37,7 @@ describe Projects::CreateFromTemplateService do
let(:project) { subject.execute } let(:project) { subject.execute }
before do before do
expect(project).to be_saved expect(project).not_to be_saved
end end
it 'does not set import set import type' do it 'does not set import set import type' do
......
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