Skip to content
Snippets Groups Projects
Commit d4ff33f4 authored by Quang-Minh Nguyen's avatar Quang-Minh Nguyen
Browse files

Add repository actor support to Feature api

In gitlab-org/gitaly#4459, we introduced
Repository actor for Gitaly. This actor targets a particular repository
managed by Gitaly. It uses repository relative path as unique identity
for flipper_id. This commit adds the support to enable/disable a flag
for a repository to admin Feature API.

Issue: gitlab-org/gitaly#4549


Changelog: added
EE: true
Signed-off-by: Quang-Minh Nguyen's avatarQuang-Minh Nguyen <qmnguyen@gitlab.com>
parent fb1b6f78
No related branches found
No related tags found
2 merge requests!103838Draft: Run test in MR with ce570f0 merging into fef465,!102744Add repository actor support to Feature api
......@@ -111,20 +111,21 @@ percentage of time.
POST /features/:name
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `name` | string | yes | Name of the feature to create or update |
| `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time |
| `key` | string | no | `percentage_of_actors` or `percentage_of_time` (default) |
| `feature_group` | string | no | A Feature group name |
| `user` | string | no | A GitLab username or comma-separated multiple usernames |
| `group` | string | no | A GitLab group's path, for example `gitlab-org`, or comma-separated multiple group paths |
| `namespace` | string | no | A GitLab group or user namespace's path, for example `john-doe`, or comma-separated multiple namespace paths. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/353117) in GitLab 15.0. |
| `project` | string | no | A projects path, for example `gitlab-org/gitlab-foss`, or comma-separated multiple project paths |
| `force` | boolean | no | Skip feature flag validation checks, such as a YAML definition |
| Attribute | Type | Required | Description |
|-----------------|----------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name` | string | yes | Name of the feature to create or update |
| `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time |
| `key` | string | no | `percentage_of_actors` or `percentage_of_time` (default) |
| `feature_group` | string | no | A Feature group name |
| `user` | string | no | A GitLab username or comma-separated multiple usernames |
| `group` | string | no | A GitLab group's path, for example `gitlab-org`, or comma-separated multiple group paths |
| `namespace` | string | no | A GitLab group or user namespace's path, for example `john-doe`, or comma-separated multiple namespace paths. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/353117) in GitLab 15.0. |
| `project` | string | no | A projects path, for example `gitlab-org/gitlab-foss`, or comma-separated multiple project paths |
| `repository` | string | no | A repository path, for example `gitlab-org/gitlab-test.git`, `gitlab-org/gitlab-test.wiki.git`, , `snippets/21.git`, to name a few. Use comma to separate multiple repository paths |
| `force` | boolean | no | Skip feature flag validation checks, such as a YAML definition |
You can enable or disable a feature for a `feature_group`, a `user`,
a `group`, a `namespace` and a `project` in a single API call.
a `group`, a `namespace`, a `project`, and a `repository` in a single API call.
```shell
curl --data "value=30" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/features/new_library"
......
......@@ -44,4 +44,21 @@
end
end
end
describe Feature::Target do
describe '#targets' do
context 'when repository target works with group wiki' do
let_it_be(:group) { create(:group) }
subject do
described_class.new(repository: group.wiki.repository.full_path)
end
it 'returns all found targets' do
expect(subject.targets).to be_an(Array)
expect(subject.targets).to eq([group.wiki.repository])
end
end
end
end
end
......@@ -91,6 +91,10 @@ def gate_specified?(params)
optional :project,
type: String,
desc: "A projects path, for example `gitlab-org/gitlab-foss`, or comma-separated multiple project paths"
optional :repository,
type: String,
desc: "A repository path, for example `gitlab-org/gitlab-test.git`, `gitlab-org/gitlab-test.wiki.git`, " \
"`snippets/21.git`, to name a few. Use comma to separate multiple repository paths"
optional :force, type: Boolean, desc: 'Skip feature flag validation checks, such as a YAML definition'
mutually_exclusive :key, :feature_group
......@@ -98,6 +102,7 @@ def gate_specified?(params)
mutually_exclusive :key, :group
mutually_exclusive :key, :namespace
mutually_exclusive :key, :project
mutually_exclusive :key, :repository
end
post ':name' do
if Feature.enabled?(:set_feature_flag_service)
......
......@@ -301,11 +301,11 @@ def initialize(params)
end
def gate_specified?
%i(user project group feature_group namespace).any? { |key| params.key?(key) }
%i(user project group feature_group namespace repository).any? { |key| params.key?(key) }
end
def targets
[feature_group, users, projects, groups, namespaces].flatten.compact
[feature_group, users, projects, groups, namespaces, repositories].flatten.compact
end
private
......@@ -350,6 +350,17 @@ def namespaces
Namespace.without_project_namespaces.find_by_full_path(arg) || (raise UnknowTargetError, "#{arg} is not found!")
end
end
def repositories
return unless params.key?(:repository)
params[:repository].split(',').map do |arg|
container, _project, _type, _path = Gitlab::RepoPath.parse(arg)
raise UnknowTargetError, "#{arg} is not found!" if container.nil?
container.repository
end
end
end
end
......
......@@ -790,11 +790,47 @@
let(:group) { create(:group) }
let(:user_name) { project.first_owner.username }
subject { described_class.new(user: user_name, project: project.full_path, group: group.full_path) }
subject do
described_class.new(
user: user_name,
project: project.full_path,
group: group.full_path,
repository: project.repository.full_path
)
end
it 'returns all found targets' do
expect(subject.targets).to be_an(Array)
expect(subject.targets).to eq([project.first_owner, project, group])
expect(subject.targets).to eq([project.first_owner, project, group, project.repository])
end
context 'when repository target works with different types of repositories' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :wiki_repo, group: group) }
let_it_be(:project_in_user_namespace) { create(:project, namespace: create(:user).namespace) }
let(:personal_snippet) { create(:personal_snippet) }
let(:project_snippet) { create(:project_snippet, project: project) }
let(:targets) do
[
project,
project.wiki,
project_in_user_namespace,
personal_snippet,
project_snippet
]
end
subject do
described_class.new(
repository: targets.map { |t| t.repository.full_path }.join(",")
)
end
it 'returns all found targets' do
expect(subject.targets).to be_an(Array)
expect(subject.targets).to eq(targets.map(&:repository))
end
end
end
end
......
......@@ -193,7 +193,7 @@
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'actors', 'value' => ["#{actor.class}:#{actor.id}"] }
{ 'key' => 'actors', 'value' => [actor.flipper_id] }
],
'definition' => known_feature_flag_definition_hash
)
......@@ -269,6 +269,20 @@
end
end
context 'when enabling for a repository by path' do
context 'when the repository exists' do
it_behaves_like 'enables the flag for the actor', :repository do
let_it_be(:actor) { create(:project).repository }
end
end
context 'when the repository does not exist' do
it_behaves_like 'does not enable the flag', :repository do
let(:actor_path) { 'not/a/repository' }
end
end
end
context 'with multiple users' do
let_it_be(:users) { create_list(:user, 3) }
......@@ -361,6 +375,29 @@
end
end
context 'with multiple repository' do
let_it_be(:projects) { create_list(:project, 3) }
it_behaves_like 'creates an enabled feature for the specified entries' do
let(:gate_params) { { repository: projects.map { |p| p.repository.full_path }.join(',') } }
let(:expected_gate_params) { projects.map { |p| p.repository.flipper_id } }
end
context 'when empty value exists between comma' do
it_behaves_like 'creates an enabled feature for the specified entries' do
let(:gate_params) { { repository: "#{projects.first.repository.full_path},,,," } }
let(:expected_gate_params) { projects.first.repository.flipper_id }
end
end
context 'when one of the projects does not exist' do
it_behaves_like 'does not enable the flag', :project do
let(:actor_path) { "#{projects.first.repository.full_path},inexistent-entry" }
let(:expected_inexistent_path) { "inexistent-entry" }
end
end
end
it 'creates a feature with the given percentage of time if passed an integer' do
post api("/features/#{feature_name}", admin), params: { value: '50' }
......
......@@ -130,6 +130,15 @@
end
end
context 'when enabling for a repository' do
let(:params) { { value: 'true', repository: project.repository.full_path } }
it 'enables the feature flag' do
expect(Feature).to receive(:enable).with(feature_name, project.repository)
expect(subject).to be_success
end
end
context 'when enabling for a user actor and a feature group' do
let(:params) { { value: 'true', user: user.username, feature_group: 'perf_team' } }
let(:feature_group) { Feature.group('perf_team') }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment