Commit 0f024c82 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'ce-to-ee-2018-08-25' into 'master'

CE upstream - 2018-08-25 06:21 UTC

See merge request gitlab-org/gitlab-ee!6986
parents a5ebf492 5c6d5f89
......@@ -2086,13 +2086,19 @@ class Project < ActiveRecord::Base
private
def rename_or_migrate_repository!
if Gitlab::CurrentSettings.hashed_storage_enabled? && storage_version != LATEST_STORAGE_VERSION
if Gitlab::CurrentSettings.hashed_storage_enabled? &&
storage_upgradable? &&
Feature.disabled?(:skip_hashed_storage_upgrade) # kill switch in case we need to disable upgrade behavior
::Projects::HashedStorageMigrationService.new(self, full_path_was).execute
else
storage.rename_repo
end
end
def storage_upgradable?
storage_version != LATEST_STORAGE_VERSION
end
def after_rename_repository(full_path_before, path_before)
execute_rename_repository_hooks!(full_path_before)
......
......@@ -5,6 +5,8 @@ class ProtectedTag < ActiveRecord::Base
include ProtectedRef
include EE::ProtectedRef
validates :name, uniqueness: { scope: :project_id }
protected_ref_access_levels :create
def self.protected?(project, ref_name)
......
---
title: Feature flag to disable Hashed Storage migration when renaming a repository
merge_request: 21291
author:
type: added
---
title: 'API: Protected tags'
merge_request: 14986
author: Robert Schilling
type: added
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class DropDuplicateProtectedTags < ActiveRecord::Migration
DOWNTIME = false
disable_ddl_transaction!
BATCH_SIZE = 1000
class Project < ActiveRecord::Base
self.table_name = 'projects'
include ::EachBatch
end
class ProtectedTag < ActiveRecord::Base
self.table_name = 'protected_tags'
end
def up
Project.each_batch(of: BATCH_SIZE) do |projects|
ids = ProtectedTag
.where(project_id: projects)
.group(:name, :project_id)
.select('max(id)')
tags = ProtectedTag
.where(project_id: projects)
.where.not(id: ids)
if Gitlab::Database.postgresql?
tags.delete_all
else
# Workaround needed for MySQL
sql = "SELECT id FROM (#{tags.to_sql}) protected_tags"
ProtectedTag.where("id IN (#{sql})").delete_all # rubocop:disable GitlabSecurity/SqlInjection
end
end
end
def down
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddProtectedTagsIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :protected_tags, [:project_id, :name], unique: true
end
def down
remove_concurrent_index :protected_tags, [:project_id, :name]
end
end
......@@ -2368,6 +2368,7 @@ ActiveRecord::Schema.define(version: 20180816193530) do
t.datetime "updated_at", null: false
end
add_index "protected_tags", ["project_id", "name"], name: "index_protected_tags_on_project_id_and_name", unique: true, using: :btree
add_index "protected_tags", ["project_id"], name: "index_protected_tags_on_project_id", using: :btree
create_table "push_event_payloads", id: false, force: :cascade do |t|
......
......@@ -60,6 +60,7 @@ following locations:
- [Project Members](members.md)
- [Project Snippets](project_snippets.md)
- [Protected Branches](protected_branches.md)
- [Protected Tags](protected_tags.md)
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
- [Runners](runners.md)
......
# Protected tags API
>**Note:** This feature was introduced in GitLab 11.3
**Valid access levels**
Currently, these levels are recognized:
```
0 => No access
30 => Developer access
40 => Maintainer access
```
## List protected tags
Gets a list of protected tags from a project.
This function takes pagination parameters `page` and `per_page` to restrict the list of protected tags.
```
GET /projects/:id/protected_tags
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_tags'
```
Example response:
```json
[
{
"name": "release-1-0",
"create_access_levels": [
{
"access_level": 40,
"access_level_description": "Maintainers"
}
]
},
...
]
```
## Get a single protected tag or wildcard protected tag
Gets a single protected tag or wildcard protected tag.
The pagination parameters `page` and `per_page` can be used to restrict the list of protected tags.
```
GET /projects/:id/protected_tags/:name
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the tag or wildcard |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_tags/release-1-0'
```
Example response:
```json
{
"name": "release-1-0",
"create_access_levels": [
{
"access_level": 40,
"access_level_description": "Maintainers"
}
]
}
```
## Protect repository tags
Protects a single repository tag or several project repository
tags using a wildcard protected tag.
```
POST /projects/:id/protected_tags
```
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_tags?name=*-stable&create_access_level=30'
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the tag or wildcard |
| `create_access_level` | string | no | Access levels allowed to create (defaults: `40`, maintainer access level) |
Example response:
```json
{
"name": "*-stable",
"create_access_levels": [
{
"access_level": 30,
"access_level_description": "Developers + Maintainers"
}
]
}
```
## Unprotect repository tags
Unprotects the given protected tag or wildcard protected tag.
```
DELETE /projects/:id/protected_tags/:name
```
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_tags/*-stable'
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the tag |
......@@ -57,3 +57,15 @@ end
Features that are developed and are intended to be merged behind a feature flag
should not include a changelog entry. The entry should be added in the merge
request removing the feature flags.
### Specs
In the test environment `Feature.enabled?` is stubbed to always respond to `true`,
so we make sure behavior under feature flag doesn't go untested in some non-specific
contexts.
If you need to test the feature flag in a different state, you need to stub it with:
```ruby
stub_feature_flags(my_feature_flag: false)
```
......@@ -106,13 +106,13 @@ module API
mount ::API::Features
mount ::API::Files
mount ::API::GroupBoards
mount ::API::Groups
mount ::API::GroupBoards
mount ::API::GroupMilestones
mount ::API::Groups
mount ::API::GroupVariables
mount ::API::Internal
mount ::API::Issues
mount ::API::Jobs
mount ::API::JobArtifacts
mount ::API::Jobs
mount ::API::Keys
mount ::API::Labels
mount ::API::Lint
......@@ -133,11 +133,12 @@ module API
mount ::API::ProjectExport
mount ::API::ProjectImport
mount ::API::ProjectHooks
mount ::API::Projects
mount ::API::ProjectMilestones
mount ::API::Projects
mount ::API::ProjectSnapshots
mount ::API::ProjectSnippets
mount ::API::ProtectedBranches
mount ::API::ProtectedTags
mount ::API::Repositories
mount ::API::Runner
mount ::API::Runners
......@@ -154,7 +155,6 @@ module API
mount ::API::Triggers
mount ::API::Users
mount ::API::Variables
mount ::API::GroupVariables
mount ::API::Version
mount ::API::Wikis
......
......@@ -431,6 +431,11 @@ module API
expose :unprotect_access_levels, using: Entities::ProtectedRefAccess
end
class ProtectedTag < Grape::Entity
expose :name
expose :create_access_levels, using: Entities::ProtectedRefAccess
end
class Milestone < Grape::Entity
expose :id, :iid
expose :project_id, if: -> (entity, options) { entity&.project_id }
......
module API
class ProtectedTags < Grape::API
include PaginationParams
TAG_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
before { authorize_admin_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc "Get a project's protected tags" do
detail 'This feature was introduced in GitLab 11.3.'
success Entities::ProtectedTag
end
params do
use :pagination
end
get ':id/protected_tags' do
protected_tags = user_project.protected_tags.preload(:create_access_levels)
present paginate(protected_tags), with: Entities::ProtectedTag, project: user_project
end
desc 'Get a single protected tag' do
detail 'This feature was introduced in GitLab 11.3.'
success Entities::ProtectedTag
end
params do
requires :name, type: String, desc: 'The name of the tag or wildcard'
end
get ':id/protected_tags/:name', requirements: TAG_ENDPOINT_REQUIREMENTS do
protected_tag = user_project.protected_tags.find_by!(name: params[:name])
present protected_tag, with: Entities::ProtectedTag, project: user_project
end
desc 'Protect a single tag or wildcard' do
detail 'This feature was introduced in GitLab 11.3.'
success Entities::ProtectedTag
end
params do
requires :name, type: String, desc: 'The name of the protected tag'
optional :create_access_level, type: Integer, default: Gitlab::Access::MAINTAINER,
values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS,
desc: 'Access levels allowed to create (defaults: `40`, maintainer access level)'
end
post ':id/protected_tags' do
protected_tags_params = {
name: params[:name],
create_access_levels_attributes: [{ access_level: params[:create_access_level] }]
}
protected_tag = ::ProtectedTags::CreateService.new(user_project,
current_user,
protected_tags_params).execute
if protected_tag.persisted?
present protected_tag, with: Entities::ProtectedTag, project: user_project
else
render_api_error!(protected_tag.errors.full_messages, 422)
end
end
desc 'Unprotect a single tag' do
detail 'This feature was introduced in GitLab 11.3.'
end
params do
requires :name, type: String, desc: 'The name of the protected tag'
end
delete ':id/protected_tags/:name', requirements: TAG_ENDPOINT_REQUIREMENTS do
protected_tag = user_project.protected_tags.find_by!(name: params[:name])
destroy_conditionally!(protected_tag)
end
end
end
end
......@@ -47,7 +47,8 @@ class Feature
end
def disabled?(key, thing = nil)
!enabled?(key, thing)
# we need to make different method calls to make it easy to mock / define expectations in test mode
thing.nil? ? !enabled?(key) : !enabled?(key, thing)
end
def enable(key, thing = true)
......
require 'spec_helper'
describe Feature do
before do
# We mock all calls to .enabled? to return true in order to force all
# specs to run the feature flag gated behavior, but here we need a clean
# behavior from the class
allow(described_class).to receive(:enabled?).and_call_original
end
describe '.get' do
let(:feature) { double(:feature) }
let(:key) { 'my_feature' }
......@@ -106,4 +113,63 @@ describe Feature do
it_behaves_like 'a memoized Flipper instance'
end
end
describe '.enabled?' do
it 'returns false for undefined feature' do
expect(described_class.enabled?(:some_random_feature_flag)).to be_falsey
end
it 'returns false for existing disabled feature in the database' do
described_class.disable(:disabled_feature_flag)
expect(described_class.enabled?(:disabled_feature_flag)).to be_falsey
end
it 'returns true for existing enabled feature in the database' do
described_class.enable(:enabled_feature_flag)
expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy
end
context 'with an individual actor' do
CustomActor = Struct.new(:flipper_id)
let(:actor) { CustomActor.new(flipper_id: 'CustomActor:5') }
let(:another_actor) { CustomActor.new(flipper_id: 'CustomActor:10') }
before do
described_class.enable(:enabled_feature_flag, actor)
end
it 'returns true when same actor is informed' do
expect(described_class.enabled?(:enabled_feature_flag, actor)).to be_truthy
end
it 'returns false when different actor is informed' do
expect(described_class.enabled?(:enabled_feature_flag, another_actor)).to be_falsey
end
it 'returns false when no actor is informed' do
expect(described_class.enabled?(:enabled_feature_flag)).to be_falsey
end
end
end
describe '.disable?' do
it 'returns true for undefined feature' do
expect(described_class.disabled?(:some_random_feature_flag)).to be_truthy
end
it 'returns true for existing disabled feature in the database' do
described_class.disable(:disabled_feature_flag)
expect(described_class.disabled?(:disabled_feature_flag)).to be_truthy
end
it 'returns false for existing enabled feature in the database' do
described_class.enable(:enabled_feature_flag)
expect(described_class.disabled?(:enabled_feature_flag)).to be_falsey
end
end
end
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180711103851_drop_duplicate_protected_tags.rb')
describe DropDuplicateProtectedTags, :migration do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:protected_tags) { table(:protected_tags) }
before do
stub_const("#{described_class}::BATCH_SIZE", 2)
namespaces.create(id: 1, name: 'gitlab-org', path: 'gitlab-org')
projects.create!(id: 1, namespace_id: 1, name: 'gitlab1', path: 'gitlab1')
projects.create!(id: 2, namespace_id: 1, name: 'gitlab2', path: 'gitlab2')
end
it 'removes duplicated protected tags' do
protected_tags.create!(id: 1, project_id: 1, name: 'foo')
tag2 = protected_tags.create!(id: 2, project_id: 1, name: 'foo1')
protected_tags.create!(id: 3, project_id: 1, name: 'foo')
tag4 = protected_tags.create!(id: 4, project_id: 1, name: 'foo')
tag5 = protected_tags.create!(id: 5, project_id: 2, name: 'foo')
migrate!
expect(protected_tags.all.count).to eq 3
expect(protected_tags.all.pluck(:id)).to contain_exactly(tag2.id, tag4.id, tag5.id)
end
it 'does not remove unique protected tags' do
tag1 = protected_tags.create!(id: 1, project_id: 1, name: 'foo1')
tag2 = protected_tags.create!(id: 2, project_id: 1, name: 'foo2')
tag3 = protected_tags.create!(id: 3, project_id: 1, name: 'foo3')
migrate!
expect(protected_tags.all.count).to eq 3
expect(protected_tags.all.pluck(:id)).to contain_exactly(tag1.id, tag2.id, tag3.id)
end
end
......@@ -3326,6 +3326,7 @@ describe Project do
# call. This makes testing a bit easier.
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
stub_feature_flags(skip_hashed_storage_upgrade: false)
end
it 'renames a repository' do
......@@ -3497,6 +3498,7 @@ describe Project do
# call. This makes testing a bit easier.
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
stub_feature_flags(skip_hashed_storage_upgrade: false)
end
context 'migration to hashed storage' do
......
require 'spec_helper'
describe API::ProtectedTags do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository) }
let(:project2) { create(:project, path: 'project2', namespace: user.namespace) }
let(:protected_name) { 'feature' }
let(:tag_name) { protected_name }
let!(:protected_tag) do
create(:protected_tag, project: project, name: protected_name)
end
describe 'GET /projects/:id/protected_tags' do
let(:route) { "/projects/#{project.id}/protected_tags" }
shared_examples_for 'protected tags' do
it 'returns the protected tags' do
get api(route, user), per_page: 100
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
protected_tag_names = json_response.map { |x| x['name'] }
expected_tags_names = project.protected_tags.map { |x| x['name'] }
expect(protected_tag_names).to match_array(expected_tags_names)
end
end
context 'when authenticated as a maintainer' do
before do
project.add_maintainer(user)
end
it_behaves_like 'protected tags'
end
context 'when authenticated as a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
end
describe 'GET /projects/:id/protected_tags/:tag' do
let(:route) { "/projects/#{project.id}/protected_tags/#{tag_name}" }
shared_examples_for 'protected tag' do
it 'returns the protected tag' do
get api(route, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(tag_name)
expect(json_response['create_access_levels'][0]['access_level']).to eq(::Gitlab::Access::MAINTAINER)
end
context 'when protected tag does not exist' do
let(:tag_name) { 'unknown' }
it_behaves_like '404 response' do
let(:request) { get api(route, user) }
let(:message) { '404 Not found' }
end
end
end
context 'when authenticated as a maintainer' do
before do
project.add_maintainer(user)
end
it_behaves_like 'protected tag'
context 'when protected tag contains a wildcard' do
let(:protected_name) { 'feature*' }
it_behaves_like 'protected tag'
end
end
context 'when authenticated as a guest' do
before do
project.add_guest(user)
end
it_behaves_like '403 response' do
let(:request) { get api(route, user) }
end
end
end
describe 'POST /projects/:id/protected_tags' do
let(:tag_name) { 'new_tag' }
context 'when authenticated as a maintainer' do
before do
project.add_maintainer(user)
end
it 'protects a single tag with maintainers can create tags' do
post api("/projects/#{project.id}/protected_tags", user), name: tag_name
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(tag_name)
expect(json_response['create_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
end
it 'protects a single tag with developers can create tags' do
post api("/projects/#{project.id}/protected_tags", user),
name: tag_name, create_access_level: 30
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(tag_name)
expect(json_response['create_access_levels'][0]['access_level']).to eq(Gitlab::Access::DEVELOPER)
end
it 'protects a single tag with no one can create tags' do
post api("/projects/#{project.id}/protected_tags", user),
name: tag_name, create_access_level: 0
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(tag_name)
expect(json_response['create_access_levels'][0]['access_level']).to eq(Gitlab::Access::NO_ACCESS)
end
it 'returns a 422 error if the same tag is protected twice' do
post api("/projects/#{project.id}/protected_tags", user), name: protected_name
expect(response).to have_gitlab_http_status(422)
expect(json_response['message'][0]).to eq('Name has already been taken')
end
it 'returns 201 if the same tag is proteted on different projects' do
post api("/projects/#{project.id}/protected_tags", user), name: protected_name
post api("/projects/#{project2.id}/protected_tags", user), name: protected_name
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(protected_name)
end
context 'when tag has a wildcard in its name' do
let(:tag_name) { 'feature/*' }
it 'protects multiple tags with a wildcard in the name' do
post api("/projects/#{project.id}/protected_tags", user), name: tag_name
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(tag_name)
expect(json_response['create_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
end
end
end
context 'when authenticated as a guest' do
before do
project.add_guest(user)
end
it 'returns a 403 error if guest' do
post api("/projects/#{project.id}/protected_tags/", user), name: tag_name
expect(response).to have_gitlab_http_status(403)
end
end
end
describe 'DELETE /projects/:id/protected_tags/unprotect/:tag' do
before do
project.add_maintainer(user)
end
it 'unprotects a single tag' do
delete api("/projects/#{project.id}/protected_tags/#{tag_name}", user)
expect(response).to have_gitlab_http_status(204)
end
it_behaves_like '412 response' do
let(:request) { api("/projects/#{project.id}/protected_tags/#{tag_name}", user) }
end
it "returns 404 if tag does not exist" do
delete api("/projects/#{project.id}/protected_tags/barfoo", user)
expect(response).to have_gitlab_http_status(404)
end
context 'when tag has a wildcard in its name' do
let(:protected_name) { 'feature*' }
it 'unprotects a wildcard tag' do
delete api("/projects/#{project.id}/protected_tags/#{tag_name}", user)
expect(response).to have_gitlab_http_status(204)
end
end
end
end
......@@ -250,9 +250,20 @@ describe Projects::UpdateService, '#execute' do
expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk')
end
context 'when hashed storage enabled' do
it 'renames the project without upgrading it' do
result = update_project(project, admin, path: 'new-path')
expect(result).not_to include(status: :error)
expect(project).to be_valid
expect(project.errors).to be_empty
expect(project.disk_path).to include('new-path')
expect(project.reload.hashed_storage?(:repository)).to be_falsey
end
context 'when hashed storage is enabled' do
before do
stub_application_setting(hashed_storage_enabled: true)
stub_feature_flags(skip_hashed_storage_upgrade: false)
end
it 'migrates project to a hashed storage instead of renaming the repo to another legacy name' do
......@@ -263,6 +274,22 @@ describe Projects::UpdateService, '#execute' do
expect(project.errors).to be_empty
expect(project.reload.hashed_storage?(:repository)).to be_truthy
end
context 'when skip_hashed_storage_upgrade feature flag is enabled' do
before do
stub_feature_flags(skip_hashed_storage_upgrade: true)
end
it 'renames the project without upgrading it' do
result = update_project(project, admin, path: 'new-path')
expect(result).not_to include(status: :error)
expect(project).to be_valid
expect(project.errors).to be_empty
expect(project.disk_path).to include('new-path')
expect(project.reload.hashed_storage?(:repository)).to be_falsey
end
end
end
end
......
module StubFeatureFlags
# Stub Feature flags with `flag_name: true/false`
#
# @param [Hash] features where key is feature name and value is boolean whether enabled or not
def stub_feature_flags(features)
features.each do |feature_name, enabled|
allow(Feature).to receive(:enabled?).with(feature_name) { enabled }
......
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