summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changelogs/unreleased/zj-feature-gate-set-project-path.yml5
-rw-r--r--doc/api/features.md5
-rw-r--r--lib/api/features.rb15
-rw-r--r--lib/feature.rb38
-rw-r--r--spec/lib/feature_spec.rb14
-rw-r--r--spec/requests/api/features_spec.rb34
6 files changed, 101 insertions, 10 deletions
diff --git a/changelogs/unreleased/zj-feature-gate-set-project-path.yml b/changelogs/unreleased/zj-feature-gate-set-project-path.yml
new file mode 100644
index 00000000000..b426a2f3fe7
--- /dev/null
+++ b/changelogs/unreleased/zj-feature-gate-set-project-path.yml
@@ -0,0 +1,5 @@
+---
+title: Allow setting of feature gates per project
+merge_request: 24184
+author:
+type: added
diff --git a/doc/api/features.md b/doc/api/features.md
index 59f1005ef72..47f104e1f20 100644
--- a/doc/api/features.md
+++ b/doc/api/features.md
@@ -60,9 +60,10 @@ POST /features/:name
| `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 |
| `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
-`user` with a single API call.
+Note that you can enable or disable a feature for a `feature_group`, a `user`,
+and a `project` in a single API call.
```bash
curl --data "value=30" --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/features/new_library
diff --git a/lib/api/features.rb b/lib/api/features.rb
index 1331248699f..835aac05905 100644
--- a/lib/api/features.rb
+++ b/lib/api/features.rb
@@ -16,15 +16,13 @@ module API
end
end
- # rubocop: disable CodeReuse/ActiveRecord
def gate_targets(params)
- targets = []
- targets << Feature.group(params[:feature_group]) if params[:feature_group]
- targets << UserFinder.new(params[:user]).find_by_username if params[:user]
+ Feature::Target.new(params).targets
+ end
- targets
+ def gate_specified?(params)
+ Feature::Target.new(params).gate_specified?
end
- # rubocop: enable CodeReuse/ActiveRecord
end
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'
optional :feature_group, type: String, desc: 'A Feature group name'
optional :user, type: String, desc: 'A GitLab username'
+ optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce'
end
post ':name' do
feature = Feature.get(params[:name])
@@ -52,13 +51,13 @@ module API
case value
when true
- if targets.present?
+ if gate_specified?(params)
targets.each { |target| feature.enable(target) }
else
feature.enable
end
when false
- if targets.present?
+ if gate_specified?(params)
targets.each { |target| feature.disable(target) }
else
feature.disable
diff --git a/lib/feature.rb b/lib/feature.rb
index e048a443abc..e59cd70f822 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -102,4 +102,42 @@ class Feature
expires_in: 1.hour)
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
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 9d56c62ae57..630732614b2 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -182,4 +182,18 @@ describe Feature do
expect(described_class.disabled?(:enabled_feature_flag)).to be_falsey
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
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index 7d3eff7d32d..22a9e36ca31 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -129,6 +129,40 @@ describe API::Features do
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
post api("/features/#{feature_name}", admin), params: { value: '50' }