Merge branch 'zj-feature-gate-set-project-path' into 'master'
Allow setting of feature gates per project See merge request gitlab-org/gitlab-ce!24184
This commit is contained in:
commit
28ca155be9
6 changed files with 102 additions and 11 deletions
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Allow setting of feature gates per project
|
||||||
|
merge_request: 24184
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -60,9 +60,10 @@ POST /features/:name
|
||||||
| `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time |
|
| `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time |
|
||||||
| `feature_group` | string | no | A Feature group name |
|
| `feature_group` | string | no | A Feature group name |
|
||||||
| `user` | string | no | A GitLab username |
|
| `user` | string | no | A GitLab username |
|
||||||
|
| `project` | string | no | A projects path, for example 'gitlab-org/gitlab-ce' |
|
||||||
|
|
||||||
Note that you can enable or disable a feature for both a `feature_group` and a
|
Note that you can enable or disable a feature for a `feature_group`, a `user`,
|
||||||
`user` with a single API call.
|
and a `project` in a single API call.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl --data "value=30" --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/features/new_library
|
curl --data "value=30" --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/features/new_library
|
||||||
|
|
|
@ -16,15 +16,13 @@ module API
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
|
||||||
def gate_targets(params)
|
def gate_targets(params)
|
||||||
targets = []
|
Feature::Target.new(params).targets
|
||||||
targets << Feature.group(params[:feature_group]) if params[:feature_group]
|
end
|
||||||
targets << UserFinder.new(params[:user]).find_by_username if params[:user]
|
|
||||||
|
def gate_specified?(params)
|
||||||
targets
|
Feature::Target.new(params).gate_specified?
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :features do
|
resource :features do
|
||||||
|
@ -44,6 +42,7 @@ module API
|
||||||
requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time'
|
requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time'
|
||||||
optional :feature_group, type: String, desc: 'A Feature group name'
|
optional :feature_group, type: String, desc: 'A Feature group name'
|
||||||
optional :user, type: String, desc: 'A GitLab username'
|
optional :user, type: String, desc: 'A GitLab username'
|
||||||
|
optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce'
|
||||||
end
|
end
|
||||||
post ':name' do
|
post ':name' do
|
||||||
feature = Feature.get(params[:name])
|
feature = Feature.get(params[:name])
|
||||||
|
@ -52,13 +51,13 @@ module API
|
||||||
|
|
||||||
case value
|
case value
|
||||||
when true
|
when true
|
||||||
if targets.present?
|
if gate_specified?(params)
|
||||||
targets.each { |target| feature.enable(target) }
|
targets.each { |target| feature.enable(target) }
|
||||||
else
|
else
|
||||||
feature.enable
|
feature.enable
|
||||||
end
|
end
|
||||||
when false
|
when false
|
||||||
if targets.present?
|
if gate_specified?(params)
|
||||||
targets.each { |target| feature.disable(target) }
|
targets.each { |target| feature.disable(target) }
|
||||||
else
|
else
|
||||||
feature.disable
|
feature.disable
|
||||||
|
|
|
@ -102,4 +102,42 @@ class Feature
|
||||||
expires_in: 1.hour)
|
expires_in: 1.hour)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class Target
|
||||||
|
attr_reader :params
|
||||||
|
|
||||||
|
def initialize(params)
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def gate_specified?
|
||||||
|
%i(user project feature_group).any? { |key| params.key?(key) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def targets
|
||||||
|
[feature_group, user, project].compact
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
def feature_group
|
||||||
|
return unless params.key?(:feature_group)
|
||||||
|
|
||||||
|
Feature.group(params[:feature_group])
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
def user
|
||||||
|
return unless params.key?(:user)
|
||||||
|
|
||||||
|
UserFinder.new(params[:user]).find_by_username!
|
||||||
|
end
|
||||||
|
|
||||||
|
def project
|
||||||
|
return unless params.key?(:project)
|
||||||
|
|
||||||
|
Project.find_by_full_path(params[:project])
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -182,4 +182,18 @@ describe Feature do
|
||||||
expect(described_class.disabled?(:enabled_feature_flag)).to be_falsey
|
expect(described_class.disabled?(:enabled_feature_flag)).to be_falsey
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe Feature::Target do
|
||||||
|
describe '#targets' do
|
||||||
|
let(:project) { create(:project) }
|
||||||
|
let(:user_name) { project.owner.username }
|
||||||
|
|
||||||
|
subject { described_class.new(user: user_name, project: project.full_path) }
|
||||||
|
|
||||||
|
it 'returns all found targets' do
|
||||||
|
expect(subject.targets).to be_an(Array)
|
||||||
|
expect(subject.targets).to eq([project.owner, project])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -129,6 +129,40 @@ describe API::Features do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when enabling for a project by path' do
|
||||||
|
context 'when the project exists' do
|
||||||
|
let!(:project) { create(:project) }
|
||||||
|
|
||||||
|
it 'sets the feature gate' do
|
||||||
|
post api("/features/#{feature_name}", admin), params: { value: 'true', project: project.full_path }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(201)
|
||||||
|
expect(json_response).to eq(
|
||||||
|
'name' => 'my_feature',
|
||||||
|
'state' => 'conditional',
|
||||||
|
'gates' => [
|
||||||
|
{ 'key' => 'boolean', 'value' => false },
|
||||||
|
{ 'key' => 'actors', 'value' => ["Project:#{project.id}"] }
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the project does not exist' do
|
||||||
|
it 'sets no new values' do
|
||||||
|
post api("/features/#{feature_name}", admin), params: { value: 'true', project: 'mep/to/the/mep/mep' }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(201)
|
||||||
|
expect(json_response).to eq(
|
||||||
|
"name" => "my_feature",
|
||||||
|
"state" => "off",
|
||||||
|
"gates" => [
|
||||||
|
{ "key" => "boolean", "value" => false }
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'creates a feature with the given percentage if passed an integer' do
|
it 'creates a feature with the given percentage if passed an integer' do
|
||||||
post api("/features/#{feature_name}", admin), params: { value: '50' }
|
post api("/features/#{feature_name}", admin), params: { value: '50' }
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue