From a94bfc79bcf2adfa80171f7425da40e8cf717c43 Mon Sep 17 00:00:00 2001 From: Fabio Pitino Date: Mon, 31 Jan 2022 19:14:16 +0000 Subject: [PATCH 1/2] Allow setting feature flag for a single namespace The `group` actor could only support Group models. With this change it can also support `UserNamespace` types and allow the feature flag to be set for a specific namespace. Changelog: changed --- lib/feature.rb | 2 +- spec/requests/api/features_spec.rb | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/feature.rb b/lib/feature.rb index 12b4ef07dd6a87..d406a69c9e9f59 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -277,7 +277,7 @@ def project def group return unless params.key?(:group) - Group.find_by_full_path(params[:group]) + Namespace.find_by_full_path(params[:group]) end end end diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 35dba93b7669ab..84641b0a612e05 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -224,6 +224,25 @@ end end + context 'when the target is a namespace' do + it 'sets the feature gate' do + namespace = create(:namespace) + + post api("/features/#{feature_name}", admin), params: { value: 'true', group: namespace.full_path } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to match( + 'name' => feature_name, + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'actors', 'value' => ["Namespaces::UserNamespace:#{namespace.id}"] } + ], + 'definition' => known_feature_flag_definition_hash + ) + end + end + context 'when the group does not exist' do it 'sets no new values and keeps the feature disabled' do post api("/features/#{feature_name}", admin), params: { value: 'true', group: 'not/a/group' } -- GitLab From 6a3665e5db5e81ff7b1273eeec74f391ca8e1c15 Mon Sep 17 00:00:00 2001 From: Fabio Pitino Date: Tue, 15 Feb 2022 07:21:53 +0000 Subject: [PATCH 2/2] Support a separate `namespace` target via API --- lib/feature.rb | 13 ++- spec/requests/api/features_spec.rb | 134 +++++++++++++---------------- 2 files changed, 72 insertions(+), 75 deletions(-) diff --git a/lib/feature.rb b/lib/feature.rb index d406a69c9e9f59..47fee23c7ea728 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -245,11 +245,11 @@ def initialize(params) end def gate_specified? - %i(user project group feature_group).any? { |key| params.key?(key) } + %i(user project group feature_group namespace).any? { |key| params.key?(key) } end def targets - [feature_group, user, project, group].compact + [feature_group, user, project, group, namespace].compact end private @@ -277,7 +277,14 @@ def project def group return unless params.key?(:group) - Namespace.find_by_full_path(params[:group]) + Group.find_by_full_path(params[:group]) + end + + def namespace + return unless params.key?(:namespace) + + # We are interested in Group or UserNamespace + Namespace.without_project_namespaces.find_by_full_path(params[:namespace]) end end end diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 84641b0a612e05..a265f67115ab4d 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -167,95 +167,85 @@ end end + shared_examples 'does not enable the flag' do |actor_type, actor_path| + it 'returns the current state of the flag without changes' do + post api("/features/#{feature_name}", admin), params: { value: 'true', actor_type => actor_path } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to match( + "name" => feature_name, + "state" => "off", + "gates" => [ + { "key" => "boolean", "value" => false } + ], + 'definition' => known_feature_flag_definition_hash + ) + end + end + + shared_examples 'enables the flag for the actor' do |actor_type| + it 'sets the feature gate' do + post api("/features/#{feature_name}", admin), params: { value: 'true', actor_type => actor.full_path } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to match( + 'name' => feature_name, + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'actors', 'value' => ["#{actor.class}:#{actor.id}"] } + ], + 'definition' => known_feature_flag_definition_hash + ) + 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(:created) - expect(json_response).to match( - 'name' => feature_name, - 'state' => 'conditional', - 'gates' => [ - { 'key' => 'boolean', 'value' => false }, - { 'key' => 'actors', 'value' => ["Project:#{project.id}"] } - ], - 'definition' => known_feature_flag_definition_hash - ) + it_behaves_like 'enables the flag for the actor', :project do + let(:actor) { create(:project) } 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(:created) - expect(json_response).to match( - "name" => feature_name, - "state" => "off", - "gates" => [ - { "key" => "boolean", "value" => false } - ], - 'definition' => known_feature_flag_definition_hash - ) - end + it_behaves_like 'does not enable the flag', :project, 'mep/to/the/mep/mep' end end context 'when enabling for a group by path' do context 'when the group exists' do - it 'sets the feature gate' do - group = create(:group) - - post api("/features/#{feature_name}", admin), params: { value: 'true', group: group.full_path } - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to match( - 'name' => feature_name, - 'state' => 'conditional', - 'gates' => [ - { 'key' => 'boolean', 'value' => false }, - { 'key' => 'actors', 'value' => ["Group:#{group.id}"] } - ], - 'definition' => known_feature_flag_definition_hash - ) + it_behaves_like 'enables the flag for the actor', :group do + let(:actor) { create(:group) } end end - context 'when the target is a namespace' do - it 'sets the feature gate' do - namespace = create(:namespace) - - post api("/features/#{feature_name}", admin), params: { value: 'true', group: namespace.full_path } - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to match( - 'name' => feature_name, - 'state' => 'conditional', - 'gates' => [ - { 'key' => 'boolean', 'value' => false }, - { 'key' => 'actors', 'value' => ["Namespaces::UserNamespace:#{namespace.id}"] } - ], - 'definition' => known_feature_flag_definition_hash - ) + context 'when the group does not exist' do + it_behaves_like 'does not enable the flag', :group, 'not/a/group' + end + end + + context 'when enabling for a namespace by path' do + context 'when the user namespace exists' do + it_behaves_like 'enables the flag for the actor', :namespace do + let(:actor) { create(:namespace) } end end - context 'when the group does not exist' do - it 'sets no new values and keeps the feature disabled' do - post api("/features/#{feature_name}", admin), params: { value: 'true', group: 'not/a/group' } - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to match( - "name" => feature_name, - "state" => "off", - "gates" => [ - { "key" => "boolean", "value" => false } - ], - 'definition' => known_feature_flag_definition_hash - ) + context 'when the group namespace exists' do + it_behaves_like 'enables the flag for the actor', :namespace do + let(:actor) { create(:group) } + end + end + + context 'when the user namespace does not exist' do + it_behaves_like 'does not enable the flag', :namespace, 'not/a/group' + end + + context 'when a project namespace exists' do + let(:project_namespace) { create(:project_namespace) } + + it_behaves_like 'does not enable the flag', :namespace do + let(:actor_path) { project_namespace.full_path } end end end -- GitLab