Commit eb9df3de authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'new-ff-crud-get' into 'master'

Support New Feature Flags in API for GET Requests

See merge request gitlab-org/gitlab!26331
parents 13da9216 03872451
# frozen_string_literal: true # frozen_string_literal: true
class FeatureFlagsFinder class FeatureFlagsFinder
attr_reader :project, :feature_flags, :params, :current_user attr_reader :project, :params, :current_user
def initialize(project, current_user, params = {}) def initialize(project, current_user, params = {})
@project = project @project = project
@current_user = current_user @current_user = current_user
@feature_flags = project.operations_feature_flags
@params = params @params = params
end end
...@@ -24,6 +23,14 @@ class FeatureFlagsFinder ...@@ -24,6 +23,14 @@ class FeatureFlagsFinder
private private
def feature_flags
if Feature.enabled?(:feature_flags_new_version, project)
project.operations_feature_flags
else
project.operations_feature_flags.legacy_flag
end
end
def by_scope(items) def by_scope(items)
case params[:scope] case params[:scope]
when 'enabled' when 'enabled'
......
...@@ -48,7 +48,7 @@ module Operations ...@@ -48,7 +48,7 @@ module Operations
class << self class << self
def preload_relations def preload_relations
preload(:scopes) preload(:scopes, strategies: :scopes)
end end
def for_unleash_client(project, environment) def for_unleash_client(project, environment)
......
...@@ -18,13 +18,13 @@ module API ...@@ -18,13 +18,13 @@ module API
resource :feature_flag_scopes do resource :feature_flag_scopes do
desc 'Get all effective feature flags under the environment' do desc 'Get all effective feature flags under the environment' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
success EE::API::Entities::FeatureFlag::DetailedScope success EE::API::Entities::FeatureFlag::DetailedLegacyScope
end end
params do params do
requires :environment, type: String, desc: 'The environment name' requires :environment, type: String, desc: 'The environment name'
end end
get do get do
present scopes_for_environment, with: EE::API::Entities::FeatureFlag::DetailedScope present scopes_for_environment, with: EE::API::Entities::FeatureFlag::DetailedLegacyScope
end end
end end
...@@ -35,18 +35,18 @@ module API ...@@ -35,18 +35,18 @@ module API
resource :scopes do resource :scopes do
desc 'Get all scopes of a feature flag' do desc 'Get all scopes of a feature flag' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
success EE::API::Entities::FeatureFlag::Scope success EE::API::Entities::FeatureFlag::LegacyScope
end end
params do params do
use :pagination use :pagination
end end
get do get do
present paginate(feature_flag.scopes), with: EE::API::Entities::FeatureFlag::Scope present paginate(feature_flag.scopes), with: EE::API::Entities::FeatureFlag::LegacyScope
end end
desc 'Create a scope of a feature flag' do desc 'Create a scope of a feature flag' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
success EE::API::Entities::FeatureFlag::Scope success EE::API::Entities::FeatureFlag::LegacyScope
end end
params do params do
requires :environment_scope, type: String, desc: 'The environment scope of the scope' requires :environment_scope, type: String, desc: 'The environment scope of the scope'
...@@ -61,7 +61,7 @@ module API ...@@ -61,7 +61,7 @@ module API
.execute(feature_flag) .execute(feature_flag)
if result[:status] == :success if result[:status] == :success
present scope, with: EE::API::Entities::FeatureFlag::Scope present scope, with: EE::API::Entities::FeatureFlag::LegacyScope
else else
render_api_error!(result[:message], result[:http_status]) render_api_error!(result[:message], result[:http_status])
end end
...@@ -73,15 +73,15 @@ module API ...@@ -73,15 +73,15 @@ module API
resource ':environment_scope', requirements: ENVIRONMENT_SCOPE_ENDPOINT_REQUIREMENTS do resource ':environment_scope', requirements: ENVIRONMENT_SCOPE_ENDPOINT_REQUIREMENTS do
desc 'Get a scope of a feature flag' do desc 'Get a scope of a feature flag' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
success EE::API::Entities::FeatureFlag::Scope success EE::API::Entities::FeatureFlag::LegacyScope
end end
get do get do
present scope, with: EE::API::Entities::FeatureFlag::Scope present scope, with: EE::API::Entities::FeatureFlag::LegacyScope
end end
desc 'Update a scope of a feature flag' do desc 'Update a scope of a feature flag' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
success EE::API::Entities::FeatureFlag::Scope success EE::API::Entities::FeatureFlag::LegacyScope
end end
params do params do
optional :active, type: Boolean, desc: 'Whether the scope is active' optional :active, type: Boolean, desc: 'Whether the scope is active'
...@@ -100,7 +100,7 @@ module API ...@@ -100,7 +100,7 @@ module API
updated_scope = result[:feature_flag].scopes updated_scope = result[:feature_flag].scopes
.find { |scope| scope.environment_scope == params[:environment_scope] } .find { |scope| scope.environment_scope == params[:environment_scope] }
present updated_scope, with: EE::API::Entities::FeatureFlag::Scope present updated_scope, with: EE::API::Entities::FeatureFlag::LegacyScope
else else
render_api_error!(result[:message], result[:http_status]) render_api_error!(result[:message], result[:http_status])
end end
...@@ -108,7 +108,7 @@ module API ...@@ -108,7 +108,7 @@ module API
desc 'Delete a scope from a feature flag' do desc 'Delete a scope from a feature flag' do
detail 'This feature was introduced in GitLab 12.5' detail 'This feature was introduced in GitLab 12.5'
success EE::API::Entities::FeatureFlag::Scope success EE::API::Entities::FeatureFlag::LegacyScope
end end
delete do delete do
authorize_update_feature_flag! authorize_update_feature_flag!
......
...@@ -30,7 +30,9 @@ module API ...@@ -30,7 +30,9 @@ module API
.new(user_project, current_user, declared_params(include_missing: false)) .new(user_project, current_user, declared_params(include_missing: false))
.execute .execute
present paginate(feature_flags), with: EE::API::Entities::FeatureFlag present paginate(feature_flags),
with: EE::API::Entities::FeatureFlag,
feature_flags_new_version_enabled: feature_flags_new_version_enabled?
end end
desc 'Create a new feature flag' do desc 'Create a new feature flag' do
...@@ -49,15 +51,17 @@ module API ...@@ -49,15 +51,17 @@ module API
post do post do
authorize_create_feature_flag! authorize_create_feature_flag!
param = declared_params(include_missing: false) attrs = declared_params(include_missing: false)
param[:scopes_attributes] = param.delete(:scopes) if param.key?(:scopes) rename_key(attrs, :scopes, :scopes_attributes)
result = ::FeatureFlags::CreateService result = ::FeatureFlags::CreateService
.new(user_project, current_user, param) .new(user_project, current_user, attrs)
.execute .execute
if result[:status] == :success if result[:status] == :success
present result[:feature_flag], with: EE::API::Entities::FeatureFlag present result[:feature_flag],
with: EE::API::Entities::FeatureFlag,
feature_flags_new_version_enabled: feature_flags_new_version_enabled?
else else
render_api_error!(result[:message], result[:http_status]) render_api_error!(result[:message], result[:http_status])
end end
...@@ -75,7 +79,9 @@ module API ...@@ -75,7 +79,9 @@ module API
get do get do
authorize_read_feature_flag! authorize_read_feature_flag!
present feature_flag, with: EE::API::Entities::FeatureFlag present feature_flag,
with: EE::API::Entities::FeatureFlag,
feature_flags_new_version_enabled: feature_flags_new_version_enabled?
end end
desc 'Enable a strategy for a feature flag on an environment' do desc 'Enable a strategy for a feature flag on an environment' do
...@@ -134,7 +140,9 @@ module API ...@@ -134,7 +140,9 @@ module API
.execute(feature_flag) .execute(feature_flag)
if result[:status] == :success if result[:status] == :success
present result[:feature_flag], with: EE::API::Entities::FeatureFlag present result[:feature_flag],
with: EE::API::Entities::FeatureFlag,
feature_flags_new_version_enabled: feature_flags_new_version_enabled?
else else
render_api_error!(result[:message], result[:http_status]) render_api_error!(result[:message], result[:http_status])
end end
...@@ -160,8 +168,20 @@ module API ...@@ -160,8 +168,20 @@ module API
end end
def feature_flag def feature_flag
@feature_flag ||= @feature_flag ||= if Feature.enabled?(:feature_flags_new_version, user_project)
user_project.operations_feature_flags.find_by_name!(params[:name]) user_project.operations_feature_flags.find_by_name!(params[:name])
else
user_project.operations_feature_flags.legacy_flag.find_by_name!(params[:name])
end
end
def feature_flags_new_version_enabled?
Feature.enabled?(:feature_flags_new_version, user_project)
end
def rename_key(hash, old_key, new_key)
hash[new_key] = hash.delete(old_key) if hash.key?(old_key)
hash
end end
end end
end end
......
...@@ -6,9 +6,11 @@ module EE ...@@ -6,9 +6,11 @@ module EE
class FeatureFlag < Grape::Entity class FeatureFlag < Grape::Entity
expose :name expose :name
expose :description expose :description
expose :version, if: :feature_flags_new_version_enabled
expose :created_at expose :created_at
expose :updated_at expose :updated_at
expose :scopes, using: FeatureFlag::Scope expose :scopes, using: FeatureFlag::LegacyScope
expose :strategies, using: FeatureFlag::Strategy, if: :feature_flags_new_version_enabled
end end
end end
end end
......
...@@ -4,7 +4,7 @@ module EE ...@@ -4,7 +4,7 @@ module EE
module API module API
module Entities module Entities
class FeatureFlag < Grape::Entity class FeatureFlag < Grape::Entity
class DetailedScope < Scope class DetailedLegacyScope < LegacyScope
expose :name expose :name
end end
end end
......
# frozen_string_literal: true
module EE
module API
module Entities
class FeatureFlag < Grape::Entity
class LegacyScope < Grape::Entity
expose :id
expose :active
expose :environment_scope
expose :strategies
expose :created_at
expose :updated_at
end
end
end
end
end
...@@ -6,11 +6,7 @@ module EE ...@@ -6,11 +6,7 @@ module EE
class FeatureFlag < Grape::Entity class FeatureFlag < Grape::Entity
class Scope < Grape::Entity class Scope < Grape::Entity
expose :id expose :id
expose :active
expose :environment_scope expose :environment_scope
expose :strategies
expose :created_at
expose :updated_at
end end
end end
end end
......
# frozen_string_literal: true
module EE
module API
module Entities
class FeatureFlag < Grape::Entity
class Strategy < Grape::Entity
expose :id
expose :name
expose :parameters
expose :scopes, using: FeatureFlag::Scope
end
end
end
end
end
...@@ -5,5 +5,13 @@ FactoryBot.define do ...@@ -5,5 +5,13 @@ FactoryBot.define do
sequence(:name) { |n| "feature_flag_#{n}" } sequence(:name) { |n| "feature_flag_#{n}" }
project project
active { true } active { true }
trait :legacy_flag do
version { Operations::FeatureFlag.versions['legacy_flag'] }
end
trait :new_version_flag do
version { Operations::FeatureFlag.versions['new_version_flag'] }
end
end end
end end
...@@ -74,5 +74,23 @@ describe FeatureFlagsFinder do ...@@ -74,5 +74,23 @@ describe FeatureFlagsFinder do
subject subject
end end
end end
context 'when new version flags are enabled' do
let!(:feature_flag_3) { create(:operations_feature_flag, :new_version_flag, name: 'flag-c', project: project) }
it 'returns new and legacy flags' do
is_expected.to eq([feature_flag_1, feature_flag_2, feature_flag_3])
end
end
context 'when new version flags are disabled' do
let!(:feature_flag_3) { create(:operations_feature_flag, :new_version_flag, name: 'flag-c', project: project) }
it 'returns only legacy flags' do
stub_feature_flags(feature_flags_new_version: false)
is_expected.to eq([feature_flag_1, feature_flag_2])
end
end
end end
end end
...@@ -4,9 +4,11 @@ ...@@ -4,9 +4,11 @@
"properties": { "properties": {
"name": { "type": "string" }, "name": { "type": "string" },
"description": { "type": ["string", "null"] }, "description": { "type": ["string", "null"] },
"version": { "type": "string" },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"updated_at": { "type": "date" }, "updated_at": { "type": "date" },
"scopes": { "type": "array", "items": { "$ref": "feature_flag_scope.json" } } "scopes": { "type": "array", "items": { "$ref": "feature_flag_scope.json" } },
"strategies": { "type": "array", "items": { "$ref": "operations/strategy.json" } }
}, },
"additionalProperties": false "additionalProperties": false
} }
{
"type": "object",
"required": ["environment_scope"],
"properties": {
"id": { "type": "integer" },
"environment_scope": { "type": "string" }
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"name",
"parameters"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"parameters": { "type": "object" },
"scopes": { "type": "array", "items": { "$ref": "scope.json" } }
},
"additionalProperties": false
}
...@@ -59,6 +59,34 @@ describe API::FeatureFlags do ...@@ -59,6 +59,34 @@ describe API::FeatureFlags do
expect(json_response.second['name']).to eq(feature_flag_2.name) expect(json_response.second['name']).to eq(feature_flag_2.name)
end end
it 'returns the legacy flag version' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flags', dir: 'ee')
expect(json_response.map { |f| f['version'] }).to eq(%w[legacy_flag legacy_flag])
end
it 'does not return the legacy flag version when the feature flag is disabled' do
stub_feature_flags(feature_flags_new_version: false)
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flags', dir: 'ee')
expect(json_response.select { |f| f.key?('version') }).to eq([])
end
it 'does not return strategies if the new flag is disabled' do
stub_feature_flags(feature_flags_new_version: false)
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flags', dir: 'ee')
expect(json_response.select { |f| f.key?('strategies') }).to eq([])
end
it 'does not have N+1 problem' do it 'does not have N+1 problem' do
control_count = ActiveRecord::QueryRecorder.new { subject } control_count = ActiveRecord::QueryRecorder.new { subject }
...@@ -70,6 +98,143 @@ describe API::FeatureFlags do ...@@ -70,6 +98,143 @@ describe API::FeatureFlags do
it_behaves_like 'check user permission' it_behaves_like 'check user permission'
end end
context 'with version 2 feature flags' do
let!(:feature_flag) do
create(:operations_feature_flag, :new_version_flag, project: project, name: 'feature1')
end
let!(:strategy) do
create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
end
let!(:scope) do
create(:operations_scope, strategy: strategy, environment_scope: 'production')
end
it 'returns the feature flags' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flags', dir: 'ee')
expect(json_response).to eq([{
'name' => 'feature1',
'description' => nil,
'version' => 'new_version_flag',
'updated_at' => feature_flag.updated_at.as_json,
'created_at' => feature_flag.created_at.as_json,
'scopes' => [],
'strategies' => [{
'id' => strategy.id,
'name' => 'default',
'parameters' => {},
'scopes' => [{
'id' => scope.id,
'environment_scope' => 'production'
}]
}]
}])
end
it 'does not return a version 2 flag when the feature flag is disabled' do
stub_feature_flags(feature_flags_new_version: false)
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flags', dir: 'ee')
expect(json_response).to eq([])
end
end
context 'with version 1 and 2 feature flags' do
it 'returns both versions of flags ordered by name' do
create(:operations_feature_flag, project: project, name: 'legacy_flag')
feature_flag = create(:operations_feature_flag, :new_version_flag, project: project, name: 'new_version_flag')
strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy, environment_scope: 'production')
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flags', dir: 'ee')
expect(json_response.map { |f| f['name'] }).to eq(%w[legacy_flag new_version_flag])
end
it 'returns only version 1 flags when the feature flag is disabled' do
stub_feature_flags(feature_flags_new_version: false)
create(:operations_feature_flag, project: project, name: 'legacy_flag')
feature_flag = create(:operations_feature_flag, :new_version_flag, project: project, name: 'new_version_flag')
strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy, environment_scope: 'production')
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flags', dir: 'ee')
expect(json_response.map { |f| f['name'] }).to eq(['legacy_flag'])
end
end
end
describe 'GET /projects/:id/feature_flags/:name' do
subject { get api("/projects/#{project.id}/feature_flags/#{feature_flag.name}", user) }
context 'when there is a feature flag' do
let!(:feature_flag) { create_flag(project, 'awesome-feature') }
it 'returns a feature flag entry' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flag', dir: 'ee')
expect(json_response['name']).to eq(feature_flag.name)
expect(json_response['description']).to eq(feature_flag.description)
expect(json_response['version']).to eq('legacy_flag')
end
it_behaves_like 'check user permission'
end
context 'with a version 2 feature_flag' do
it 'returns the feature flag' do
feature_flag = create(:operations_feature_flag, :new_version_flag, project: project, name: 'feature1')
strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
scope = create(:operations_scope, strategy: strategy, environment_scope: 'production')
get api("/projects/#{project.id}/feature_flags/feature1", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flag', dir: 'ee')
expect(json_response).to eq({
'name' => 'feature1',
'description' => nil,
'version' => 'new_version_flag',
'updated_at' => feature_flag.updated_at.as_json,
'created_at' => feature_flag.created_at.as_json,
'scopes' => [],
'strategies' => [{
'id' => strategy.id,
'name' => 'default',
'parameters' => {},
'scopes' => [{
'id' => scope.id,
'environment_scope' => 'production'
}]
}]
})
end
it 'returns a 404 when the feature is disabled' do
stub_feature_flags(feature_flags_new_version: false)
feature_flag = create(:operations_feature_flag, :new_version_flag, project: project, name: 'feature1')
strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {})
create(:operations_scope, strategy: strategy, environment_scope: 'production')
get api("/projects/#{project.id}/feature_flags/feature1", user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq({ 'message' => '404 Not found' })
end
end
end end
describe 'POST /projects/:id/feature_flags' do describe 'POST /projects/:id/feature_flags' do
...@@ -97,6 +262,24 @@ describe API::FeatureFlags do ...@@ -97,6 +262,24 @@ describe API::FeatureFlags do
it_behaves_like 'check user permission' it_behaves_like 'check user permission'
it 'returns version' do
subject
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/feature_flag', dir: 'ee')
expect(json_response['version']).to eq('legacy_flag')
end
it 'does not return version when new version flags are disabled' do
stub_feature_flags(feature_flags_new_version: false)
subject
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/feature_flag', dir: 'ee')
expect(json_response.key?('version')).to eq(false)
end
context 'when no scopes passed in parameters' do context 'when no scopes passed in parameters' do
let(:params) { { name: 'awesome-feature' } } let(:params) { { name: 'awesome-feature' } }
...@@ -167,25 +350,6 @@ describe API::FeatureFlags do ...@@ -167,25 +350,6 @@ describe API::FeatureFlags do
end end
end end
describe 'GET /projects/:id/feature_flags/:name' do
subject { get api("/projects/#{project.id}/feature_flags/#{feature_flag.name}", user) }
context 'when there is a feature flag' do
let!(:feature_flag) { create_flag(project, 'awesome-feature') }
it 'returns a feature flag entry' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flag', dir: 'ee')
expect(json_response['name']).to eq(feature_flag.name)
expect(json_response['description']).to eq(feature_flag.description)
end
it_behaves_like 'check user permission'
end
end
describe 'POST /projects/:id/feature_flags/:name/enable' do describe 'POST /projects/:id/feature_flags/:name/enable' do
subject do subject do
post api("/projects/#{project.id}/feature_flags/#{params[:name]}/enable", user), post api("/projects/#{project.id}/feature_flags/#{params[:name]}/enable", user),
...@@ -353,5 +517,21 @@ describe API::FeatureFlags do ...@@ -353,5 +517,21 @@ describe API::FeatureFlags do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
it 'returns version' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['version']).to eq('legacy_flag')
end
it 'does not return version when new version flags are disabled' do
stub_feature_flags(feature_flags_new_version: false)
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.key?('version')).to eq(false)
end
end end
end end
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