diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 4783b10cf6da0c322c39d1ee8e91eb90eb0b2138..8506b86a54fda2e948e3ae2168599a1040e8f778 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -11,7 +11,10 @@
.d-flex.flex-column.flex-wrap.align-items-baseline
.d-inline-flex.align-items-baseline
%h1.home-panel-title.prepend-top-8.append-bottom-5.qa-project-name
- = @project.name
+ - if Feature.enabled?(:test_project_name, @project)
+ = "The feature is on this project!"
+ - else
+ = @project.name
%span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
= visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
.home-panel-metadata.d-flex.flex-wrap.text-secondary
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 814ea551e19b63b78c66f42d0aa6220b169ea11e..f3393eb51b8b4e92ab0bb7ed1a6710bd06caa222 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -468,6 +468,13 @@ production: &base
# enabled: true
# primary_api_url: http://localhost:5000/ # internal address to the primary registry, will be used by GitLab to directly communicate with primary registry API
+ ## Feature Flag https://docs.gitlab.com/ee/user/project/operations/feature_flags.html
+ unleash:
+ # enabled: false
+ # url: https://gitlab.com/api/v4/feature_flags/unleash/
+ # app_name: gitlab.com # Environment name of your GitLab instance
+ # instance_id: INSTNACE_ID
+ # personal_access_token: PAT # Used for controlling flags on the project
#
# 2. GitLab CI settings
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index fbe6c21e53dfa5a6c40c6ffdb8968fd3d1dfda5c..1332d425373e517a8514a2becea9f641c95f97a9 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -310,6 +310,15 @@
Settings.geo.registry_replication['enabled'] ||= false
end
+#
+# Unleash
+#
+Settings['unleash'] ||= Settingslogic.new({})
+Settings.unleash['enabled'] = false if Settings.unleash['enabled'].nil?
+Settings.unleash['app_name'] = Rails.env if Settings.unleash['app_name'].nil?
+Settings.unleash['instance_id'] = ENV['UNLEASH_INSTANCE_ID'] if Settings.unleash['instance_id'].nil?
+Settings.unleash['personal_access_token'] = ENV['UNLEASH_PERSONAL_ACCESS_TOKEN'] if Settings.unleash['personal_access_token'].nil?
+
#
# External merge request diffs
#
diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb
index 80cab7273e5ccd9ac994cba58baa6300c809e5d6..73e0681081880cdec99653d1b92c8eb8ddc10c38 100644
--- a/config/initializers/flipper.rb
+++ b/config/initializers/flipper.rb
@@ -1 +1 @@
-Feature.register_feature_groups
+FeatureFlag::Adapters::Flipper.register_feature_groups
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index e3505579204b9d5d4c95d9adfbe816979361faaa..024f9ee19d761632a2ab3281c7ff722c87b1ee44 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,13 +1,13 @@
require 'sidekiq/web'
def enable_reliable_fetch?
- return true unless Feature::FlipperFeature.table_exists?
+ return true unless Feature.table_exists?
Feature.enabled?(:gitlab_sidekiq_reliable_fetcher, default_enabled: true)
end
def enable_semi_reliable_fetch_mode?
- return true unless Feature::FlipperFeature.table_exists?
+ return true unless Feature.table_exists?
Feature.enabled?(:gitlab_sidekiq_enable_semi_reliable_fetcher, default_enabled: true)
end
diff --git a/config/initializers/unleash.rb b/config/initializers/unleash.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f968da0aa9e691c2991ba38113e54ef598bd5035
--- /dev/null
+++ b/config/initializers/unleash.rb
@@ -0,0 +1,4 @@
+return unless Gitlab.config.unleash.enabled && defined?(::Unicorn)
+
+FeatureFlag::Adapters::Unleash.configure
+UNLEASH = Unleash::Client.new
diff --git a/ee/app/finders/feature_flags_finder.rb b/ee/app/finders/feature_flags_finder.rb
index 60a2ffac6ded465f8e5fd15bea87908014614af4..7a02b48e70945be62c2f4a563e3bd2a21467c70d 100644
--- a/ee/app/finders/feature_flags_finder.rb
+++ b/ee/app/finders/feature_flags_finder.rb
@@ -10,7 +10,7 @@ def initialize(project, current_user, params = {})
@params = params
end
- def execute
+ def execute(preload: false)
unless Ability.allowed?(current_user, :read_feature_flag, project)
return Operations::FeatureFlag.none
end
@@ -19,6 +19,7 @@ def execute
items = by_scope(items)
items = for_list(items)
+ items.preload_relations if preload
items.ordered
end
diff --git a/ee/app/models/operations/feature_flag_scope.rb b/ee/app/models/operations/feature_flag_scope.rb
index b8eb9e1950da69c99e5979d066fa06f179ec5c5e..08f938c9a73d1e588266ca544d74b3152366beaa 100644
--- a/ee/app/models/operations/feature_flag_scope.rb
+++ b/ee/app/models/operations/feature_flag_scope.rb
@@ -34,7 +34,7 @@ def userwithid_strategy
def self.with_name_and_description
joins(:feature_flag)
- .select(FeatureFlag.arel_table[:name], FeatureFlag.arel_table[:description])
+ .select(::Operations::FeatureFlag.arel_table[:name], ::Operations::FeatureFlag.arel_table[:description])
end
def self.for_unleash_client(project, environment)
diff --git a/ee/lib/api/feature_flag/scopes.rb b/ee/lib/api/feature_flag/scopes.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e7f9108c4fc6895dbb188ad5bcdcb6e7b2df8665
--- /dev/null
+++ b/ee/lib/api/feature_flag/scopes.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+module API
+ module FeatureFlag
+ class Scopes < Grape::API
+ include PaginationParams
+
+ FEATURE_FLAG_ENDPOINT_REQUIREMETS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS
+ .merge(name: API::NO_SLASH_URL_PART_REGEX)
+
+ before { authorize_read_feature_flags! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get all effective scopes under the project' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag::DetailedScope
+ end
+ params do
+ requires :environment_scope, type: String, desc: 'The environment scope'
+ end
+ get ':id/feature_flag_scopes' do
+ present scopes_for_environment, with: EE::API::Entities::FeatureFlag::DetailedScope
+ end
+
+ params do
+ requires :name, type: String, desc: 'The name of the feature flag'
+ end
+ resource ':id/feature_flags/:name/scopes', requirements: FEATURE_FLAG_ENDPOINT_REQUIREMETS do
+ desc 'Get all scopes of a feature flag' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag::Scope
+ end
+ params do
+ use :pagination
+ end
+ get do
+ present paginate(feature_flag.scopes), with: EE::API::Entities::FeatureFlag::Scope
+ end
+
+ desc 'Get a scope of a feature flag' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag::Scope
+ end
+ params do
+ requires :scope_id, type: String
+ end
+ get ':scope_id' do
+ present scope, with: EE::API::Entities::FeatureFlag::Scope
+ end
+
+ desc 'Create a new scope for a feature flag' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag::Scope
+ end
+ params do
+ requires :environment_scope, type: String
+ requires :active, type: Boolean
+ requires :strategies, type: JSON
+ end
+ post do
+ authorize_update_feature_flag!
+
+ param = { scopes_attributes: [declared_params(include_missing: false)] }
+
+ result = ::FeatureFlags::UpdateService
+ .new(user_project, current_user, param)
+ .execute(feature_flag)
+
+ if result[:status] == :success
+ present result[:feature_flag].scopes.last, with: EE::API::Entities::FeatureFlag::Scope
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
+ desc 'Update a scope of a feature flag' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag::Scope
+ end
+ params do
+ requires :scope_id, type: String
+ optional :environment_scope, type: String
+ optional :active, type: Boolean
+ optional :strategies, type: JSON
+ end
+ put ':scope_id' do
+ authorize_update_feature_flag!
+
+ param = declared_params(include_missing: false)
+ param[:id] = param.delete(:scope_id)
+ param = { scopes_attributes: [param] }
+
+ result = ::FeatureFlags::UpdateService
+ .new(user_project, current_user, param)
+ .execute(feature_flag)
+
+ if result[:status] == :success
+ present scope.reload, with: EE::API::Entities::FeatureFlag::Scope
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
+ desc 'Delete a scope from a feature flag' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag::Scope
+ end
+ params do
+ optional :scope_id, type: String, desc: 'The scope'
+ end
+ delete ':scope_id' do
+ authorize_update_feature_flag!
+
+ param = { scopes_attributes: [{ id: scope.id, _destroy: 1 }] }
+
+ result = ::FeatureFlags::UpdateService
+ .new(user_project, current_user, param)
+ .execute(feature_flag)
+
+ if result[:status] == :success
+ present scope, with: EE::API::Entities::FeatureFlag::Scope
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+ end
+ end
+
+ helpers do
+ def authorize_read_feature_flags!
+ authorize! :read_feature_flag, user_project
+ end
+
+ def authorize_update_feature_flag!
+ authorize! :update_feature_flag, feature_flag
+ end
+
+ def feature_flag
+ @feature_flag ||= user_project.operations_feature_flags.find_by_name!(params[:name])
+ end
+
+ def scope
+ @scope ||= feature_flag.scopes.find_by_id!(params[:scope_id])
+ end
+
+ def scopes_for_environment
+ Operations::FeatureFlagScope.for_unleash_client(user_project, params[:environment_scope])
+ end
+ end
+ end
+ end
+end
diff --git a/ee/lib/api/feature_flags.rb b/ee/lib/api/feature_flags.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d30198751068a4fe24d540ce056cc87024c652e7
--- /dev/null
+++ b/ee/lib/api/feature_flags.rb
@@ -0,0 +1,279 @@
+# frozen_string_literal: true
+
+module API
+ class FeatureFlags < Grape::API
+ include PaginationParams
+
+ FEATURE_FLAG_ENDPOINT_REQUIREMETS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS
+ .merge(name: API::NO_SLASH_URL_PART_REGEX)
+
+ before { authorize_read_feature_flags! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get all feature flags of a project' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag
+ end
+ params do
+ optional :scope, type: String, desc: 'The scope', values: %w[enabled disabled]
+ use :pagination
+ end
+ get ':id/feature_flags' do
+ feature_flags = ::FeatureFlagsFinder
+ .new(user_project, current_user, params)
+ .execute(preload: true)
+
+ present paginate(feature_flags), with: EE::API::Entities::FeatureFlag
+ end
+
+ desc 'Get a feature flag of a project' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the feature flag'
+ end
+ get ':id/feature_flags/:name', requirements: FEATURE_FLAG_ENDPOINT_REQUIREMETS do
+ present feature_flag, with: EE::API::Entities::FeatureFlag
+ end
+
+ desc 'Create a new feature flag' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag
+ end
+ params do
+ requires :name, type: String
+ optional :description, type: String
+ optional :scopes_attributes, type: Array do
+ requires :environment_scope, type: String
+ requires :active, type: Boolean
+ requires :strategies, type: JSON
+ end
+ end
+ post ':id/feature_flags' do
+ authorize_create_feature_flag!
+
+ result = ::FeatureFlags::CreateService
+ .new(user_project, current_user, declared_params(include_missing: false))
+ .execute
+
+ if result[:status] == :success
+ present result[:feature_flag], with: EE::API::Entities::FeatureFlag
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
+ desc 'Update a feature flag' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag
+ end
+ params do
+ optional :new_name, type: String
+ optional :description, type: String
+ at_least_one_of :new_name, :description
+ end
+ put ':id/feature_flags/:name', requirements: FEATURE_FLAG_ENDPOINT_REQUIREMETS do
+ authorize_update_feature_flag!
+
+ result = ::FeatureFlags::UpdateService
+ .new(user_project, current_user, declared_params(include_missing: false))
+ .execute(feature_flag)
+
+ if result[:status] == :success
+ present result[:feature_flag], with: EE::API::Entities::FeatureFlag
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
+ desc 'Enable a feature flag for an environment with strategy' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag
+ end
+ params do
+ requires :name, type: String
+ requires :environment_scope, type: String
+ requires :strategy, type: JSON
+ end
+ post ':id/feature_flags/:name/enable', requirements: FEATURE_FLAG_ENDPOINT_REQUIREMETS do
+ authorize_create_feature_flag!
+
+ result = nil
+
+ if feature_flag = user_project.operations_feature_flags.find_by_name(params[:name])
+ scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
+
+ update_params = unless scope
+ {
+ scopes_attributes:[{
+ active: true,
+ environment_scope: params[:environment_scope],
+ strategies: [params[:strategy]]
+ }]
+ }
+ else
+ {
+ scopes_attributes:[{
+ id: scope.id,
+ active: true,
+ strategies: scope.strategies.push(params[:strategy]).uniq
+ }]
+ }
+ end
+
+ result = ::FeatureFlags::UpdateService
+ .new(user_project, current_user, update_params)
+ .execute(feature_flag)
+ else
+ create_params = {
+ name: params[:name],
+ scopes_attributes:[{
+ active: false,
+ environment_scope: '*'
+ },{
+ active: true,
+ environment_scope: params[:environment_scope],
+ strategies: [params[:strategy]]
+ }]
+ }
+
+ result = ::FeatureFlags::CreateService
+ .new(user_project, current_user, create_params)
+ .execute
+ end
+
+ if result.nil?
+ render_api_error!('Bad request', 400)
+ elsif result[:status] == :success
+ present result[:feature_flag], with: EE::API::Entities::FeatureFlag
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
+ desc 'Disable a feature flag for an environment with strategy' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag
+ end
+ params do
+ requires :name, type: String
+ requires :environment_scope, type: String
+ requires :strategy, type: JSON
+ end
+ post ':id/feature_flags/:name/disable', requirements: FEATURE_FLAG_ENDPOINT_REQUIREMETS do
+ authorize_create_feature_flag!
+
+ result = nil
+
+ scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
+
+ not_found! unless scope
+
+ remained_strategy = scope.strategies.reject { |str| str['name'] == params[:strategy]['name'] && str['parameters'] == params[:strategy]['parameters'] }
+
+ not_modified! if scope.strategies == remained_strategy
+
+ update_params = if remained_strategy.empty?
+ {
+ scopes_attributes:[{
+ id: scope.id,
+ _destroy: 1
+ }]
+ }
+ else
+ {
+ scopes_attributes:[{
+ id: scope.id,
+ strategies: remained_strategy
+ }]
+ }
+ end
+
+ result = ::FeatureFlags::UpdateService
+ .new(user_project, current_user, update_params)
+ .execute(feature_flag)
+
+ if result.nil?
+ render_api_error!('Bad request', 400)
+ elsif result[:status] == :success
+ present result[:feature_flag], with: EE::API::Entities::FeatureFlag
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
+ desc 'Delete a feature flag' do
+ detail 'This feature was introduced in GitLab 12.4.'
+ success EE::API::Entities::FeatureFlag
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the feature flag'
+ optional :environment_scope, type: String, desc: 'The environment scope'
+ end
+ delete ':id/feature_flags/:name', requirements: FEATURE_FLAG_ENDPOINT_REQUIREMETS do
+ authorize_destroy_feature_flag!
+
+ result = nil
+
+ if params[:environment_scope]
+ scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
+
+ not_found! unless scope
+
+ update_params = {
+ scopes_attributes:[{
+ id: scope.id,
+ _destroy: 1
+ }]
+ }
+
+ result = ::FeatureFlags::UpdateService
+ .new(user_project, current_user, update_params)
+ .execute(feature_flag)
+ else
+ result = ::FeatureFlags::DestroyService
+ .new(user_project, current_user, declared_params(include_missing: false))
+ .execute(feature_flag)
+ end
+
+ if result.nil?
+ render_api_error!('Bad request', 400)
+ elsif result[:status] == :success
+ present result[:feature_flag], with: EE::API::Entities::FeatureFlag
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+ end
+
+ helpers do
+ def authorize_read_feature_flags!
+ authorize! :read_feature_flag, user_project
+ end
+
+ def authorize_read_feature_flag!
+ authorize! :read_feature_flag, feature_flag
+ end
+
+ def authorize_create_feature_flag!
+ authorize! :create_feature_flag, user_project
+ end
+
+ def authorize_update_feature_flag!
+ authorize! :update_feature_flag, feature_flag
+ end
+
+ def authorize_destroy_feature_flag!
+ authorize! :destroy_feature_flag, feature_flag
+ end
+
+ def feature_flag
+ @feature_flag ||= user_project.operations_feature_flags.find_by_name!(params[:name])
+ end
+ end
+ end
+end
diff --git a/ee/lib/ee/api/api.rb b/ee/lib/ee/api/api.rb
index 91563ca6ec41bde110a42fcb3d9706d20de0a853..b0a1d89cfb36f28ee113bc1e3ce2e99b6aad5037 100644
--- a/ee/lib/ee/api/api.rb
+++ b/ee/lib/ee/api/api.rb
@@ -17,6 +17,8 @@ module API
mount ::API::EpicIssues
mount ::API::EpicLinks
mount ::API::Epics
+ mount ::API::FeatureFlags
+ mount ::API::FeatureFlag::Scopes
mount ::API::ContainerRegistryEvent
mount ::API::Geo
mount ::API::GeoNodes
diff --git a/ee/lib/ee/api/entities.rb b/ee/lib/ee/api/entities.rb
index c3da875234ee1c05e04798adaec79ab8d020c9c8..8b2ec554d9c8f4d1706ccad8e593f0d01f916802 100644
--- a/ee/lib/ee/api/entities.rb
+++ b/ee/lib/ee/api/entities.rb
@@ -789,6 +789,27 @@ def can_read_vulnerabilities?(user, project)
Ability.allowed?(user, :read_project_security_dashboard, project)
end
end
+
+ class FeatureFlag < Grape::Entity
+ class Scope < Grape::Entity
+ expose :id
+ expose :active
+ expose :environment_scope
+ expose :strategies
+ expose :created_at
+ expose :updated_at
+ end
+
+ class DetailedScope < Scope
+ expose :name
+ end
+
+ expose :name
+ expose :description
+ expose :created_at
+ expose :updated_at
+ expose :scopes, using: Scope
+ end
end
end
end
diff --git a/ee/spec/requests/api/feature_flag/scopes_spec.rb b/ee/spec/requests/api/feature_flag/scopes_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e1f7abac826b430cc94538757e484e3baedfb00c
--- /dev/null
+++ b/ee/spec/requests/api/feature_flag/scopes_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe API::FeatureFlag::Scopes do
+ include FeatureFlagHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:developer) { create(:user) }
+
+ before do
+ stub_licensed_features(feature_flags: true)
+
+ project.add_developer(developer)
+ end
+
+ describe 'GET /projects/:id/feature_flags/:name/scopes' do
+ context 'when there are two scopes' do
+ let!(:feature_flag) { create_flag(project, 'test') }
+ let!(:additional_scope) { create_scope(feature_flag, 'production', false) }
+
+ it 'returns scopes' do
+ get api("/projects/#{project.id}/feature_flags/#{feature_flag.name}/scopes", developer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(2)
+ expect(json_response.first['environment_scope']).to eq(feature_flag.scopes[0].environment_scope)
+ expect(json_response.second['environment_scope']).to eq(feature_flag.scopes[1].environment_scope)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/feature_flags/:name/scopes/:scope_id' do
+ context 'when there is a feature flag' do
+ let!(:feature_flag) { create(:operations_feature_flag, project: project) }
+ let(:default_scope) { feature_flag.default_scope }
+
+ it 'returns a scope' do
+ get api("/projects/#{project.id}/feature_flags/#{feature_flag.name}/scopes/#{default_scope.id}", developer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(default_scope.id)
+ expect(json_response['active']).to eq(default_scope.active)
+ expect(json_response['environment_scope']).to eq(default_scope.environment_scope)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/feature_flags/:name/scopes' do
+ let(:params) do
+ {
+ environment_scope: 'staging',
+ active: false,
+ strategies: [{
+ name: 'userWithId',
+ parameters: { userIds: 'user:1,project:1,group:1' }
+ }].to_json
+ }
+ end
+
+ let!(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+ it 'creates a new scope' do
+ post api("/projects/#{project.id}/feature_flags/#{feature_flag.name}/scopes", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+
+ scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
+ expect(scope.active).to eq(params[:active])
+ expect(scope.strategies).to eq(JSON.parse(params[:strategies]))
+ end
+ end
+
+ describe 'PUT /projects/:id/feature_flags/:name/scopes/:scope_id' do
+ let(:params) { { active: true } }
+
+ let!(:feature_flag) { create(:operations_feature_flag, project: project) }
+ let!(:production_scope) { create_scope(feature_flag, 'production', false) }
+
+ it 'updates the name' do
+ put api("/projects/#{project.id}/feature_flags/#{feature_flag.name}/scopes/#{production_scope.id}", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ production_scope.reload
+ expect(production_scope.active).to eq(true)
+ end
+ end
+
+ describe 'DELETE /projects/:id/feature_flags/:name/scopes/:scope_id' do
+ let!(:feature_flag) { create(:operations_feature_flag, project: project) }
+ let!(:production_scope) { create_scope(feature_flag, 'production', false) }
+
+ it 'destroys the scope' do
+ expect do
+ delete api("/projects/#{project.id}/feature_flags/#{feature_flag.name}/scopes/#{production_scope.id}", developer)
+ end.to change { Operations::FeatureFlagScope.count }.by(-1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
diff --git a/ee/spec/requests/api/feature_flags_spec.rb b/ee/spec/requests/api/feature_flags_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d72745f994942ba7ee089d33fcec9a9f439eb974
--- /dev/null
+++ b/ee/spec/requests/api/feature_flags_spec.rb
@@ -0,0 +1,256 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe API::FeatureFlags do
+ include FeatureFlagHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:developer) { create(:user) }
+ let(:non_project_member) { create(:user) }
+
+ before do
+ stub_licensed_features(feature_flags: true)
+
+ project.add_developer(developer)
+ end
+
+ describe 'GET /projects/:id/feature_flags' do
+ context 'when there are two feature flags' do
+ let!(:feature_flag_1) do
+ create(:operations_feature_flag, project: project)
+ end
+
+ let!(:feature_flag_2) do
+ create(:operations_feature_flag, project: project)
+ end
+
+ it 'returns feature flags' do
+ get api("/projects/#{project.id}/feature_flags", developer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(2)
+ expect(json_response.first['name']).to eq(feature_flag_1.name)
+ expect(json_response.second['name']).to eq(feature_flag_2.name)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/feature_flags/:name' do
+ context 'when there is a feature flag' do
+ let!(:feature_flag) do
+ create(:operations_feature_flag, project: project)
+ end
+
+ it 'returns a feature flag entry' do
+ get api("/projects/#{project.id}/feature_flags/#{feature_flag.name}", developer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['name']).to eq(feature_flag.name)
+ expect(json_response['description']).to eq(feature_flag.description)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/feature_flags' do
+ let(:params) do
+ {
+ name: 'awesome-feature',
+ description: 'aaaaaaaa',
+ scopes_attributes: [
+ {
+ environment_scope: '*',
+ active: true,
+ strategies: [{
+ name: 'default',
+ parameters: {}
+ }].to_json
+ },
+ {
+ environment_scope: 'production',
+ active: true,
+ strategies: [{
+ name: 'userWithId',
+ parameters: {
+ userIds: 'user:1'
+ }
+ }].to_json
+ }]
+ }
+ end
+
+ it 'creates a new feature flag' do
+ post api("/projects/#{project.id}/feature_flags", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+
+ feature_flag = project.operations_feature_flags.last
+ expect(feature_flag.name).to eq(params[:name])
+ expect(feature_flag.description).to eq(params[:description])
+
+ feature_flag.scopes.each_with_index do |scope, index|
+ expect(scope.environment_scope).to eq(params[:scopes_attributes][index][:environment_scope])
+ expect(scope.active).to eq(params[:scopes_attributes][index][:active])
+ expect(scope.strategies).to eq(JSON.parse(params[:scopes_attributes][index][:strategies]))
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/feature_flags/:name' do
+ let(:params) { { description: 'bbbb' } }
+
+ let!(:feature_flag) do
+ create(:operations_feature_flag, project: project, description: 'aaaaa')
+ end
+
+ it 'updates the name' do
+ put api("/projects/#{project.id}/feature_flags/#{feature_flag.name}", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(project.operations_feature_flags.last.description).to eq('bbbb')
+ end
+ end
+
+ describe 'POST /projects/:id/feature_flags/enable' do
+ let(:params) do
+ {
+ name: 'awesome-feature',
+ environment_scope: 'production',
+ strategy: { name: 'userWithId', parameters: { userIds: 'Project:1' } }.to_json
+ }
+ end
+
+ context 'when feature flag & scope do not exist yet' do
+ it 'creates a new feature flag and scope' do
+ post api("/projects/#{project.id}/feature_flags/enable", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+
+ feature_flag = project.operations_feature_flags.last
+ expect(feature_flag.name).to eq(params[:name])
+
+ scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
+ expect(scope.strategies).to eq([JSON.parse(params[:strategy])])
+ end
+ end
+
+ context 'when feature flag exists already' do
+ let!(:feature_flag) { create_flag(project, params[:name]) }
+
+ context 'when environment scope does not exist yet' do
+ it 'creates a new scope' do
+ post api("/projects/#{project.id}/feature_flags/enable", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+
+ scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
+ expect(scope.strategies).to eq([JSON.parse(params[:strategy])])
+ end
+ end
+
+ context 'when scope exists already' do
+ let(:defined_strategy) { { name: 'userWithId', parameters: { userIds: 'Project:2' }} }
+
+ before do
+ create_scope(feature_flag, params[:environment_scope], true, [defined_strategy])
+ end
+
+ it 'adds an additional strategy param' do
+ post api("/projects/#{project.id}/feature_flags/enable", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+
+ scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
+ expect(scope.strategies).to eq([defined_strategy.deep_stringify_keys, JSON.parse(params[:strategy])])
+ end
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/feature_flags/disable' do
+ let(:params) do
+ {
+ name: 'awesome-feature',
+ environment_scope: 'production',
+ strategy: { name: 'userWithId', parameters: { userIds: 'Project:1' } }.to_json
+ }
+ end
+
+ context 'when feature flag & scope do not exist yet' do
+ it 'returns not modified' do
+ post api("/projects/#{project.id}/feature_flags/disable", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:not_modified)
+ end
+ end
+
+ context 'when feature flag exists already' do
+ let!(:feature_flag) { create_flag(project, params[:name]) }
+
+ context 'when environment scope does not exist yet' do
+ it 'returns not modified' do
+ post api("/projects/#{project.id}/feature_flags/disable", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:not_modified)
+ end
+ end
+
+ context 'when scope exists already and can find the corresponding one' do
+ let(:defined_strategies) { [{ name: 'userWithId', parameters: { userIds: 'Project:1' }}, { name: 'userWithId', parameters: { userIds: 'Project:2' }}] }
+
+ before do
+ create_scope(feature_flag, params[:environment_scope], true, defined_strategies)
+ end
+
+ it 'removes the strategy from the scope' do
+ post api("/projects/#{project.id}/feature_flags/disable", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+
+ scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
+ expect(scope.strategies).to eq([{ name: 'userWithId', parameters: { userIds: 'Project:2' }}.deep_stringify_keys])
+ end
+
+ context 'when strategies become empty array afterward' do
+ let(:defined_strategies) { [{ name: 'userWithId', parameters: { userIds: 'Project:1' }}] }
+
+ it 'deactivates the scope' do
+ post api("/projects/#{project.id}/feature_flags/disable", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+
+ scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
+ expect(scope.active).to eq(false)
+ end
+ end
+ end
+
+ context 'when scope exists already but cannot find the corresponding one' do
+ let(:defined_strategy) { { name: 'userWithId', parameters: { userIds: 'Project:2' }} }
+
+ before do
+ create_scope(feature_flag, params[:environment_scope], true, [defined_strategy])
+ end
+
+ it 'returns not modified' do
+ post api("/projects/#{project.id}/feature_flags/disable", developer), params: params
+
+ expect(response).to have_gitlab_http_status(:not_modified)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/feature_flags/:name' do
+ let!(:feature_flag) do
+ create(:operations_feature_flag, project: project)
+ end
+
+ it 'destroys the release' do
+ expect do
+ delete api("/projects/#{project.id}/feature_flags/#{feature_flag.name}", developer)
+ end.to change { Operations::FeatureFlag.count }.by(-1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
diff --git a/ee/spec/support/helpers/feature_flag_helpers.rb b/ee/spec/support/helpers/feature_flag_helpers.rb
index e8b7cd3e58c6c8ebaedd77e90c22e38349fcf3fd..ec1907d0adcd6b6093f83416a40a17aa7deeec57 100644
--- a/ee/spec/support/helpers/feature_flag_helpers.rb
+++ b/ee/spec/support/helpers/feature_flag_helpers.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module FeatureFlagHelpers
- def create_flag(project, name, active, description: nil)
+ def create_flag(project, name, active = true, description: nil)
create(:operations_feature_flag, name: name, active: active,
description: description, project: project)
end
diff --git a/lib/feature.rb b/lib/feature.rb
index 88b0d871c3adc2b5d8d3c7fe3c0e83114e3a3d3a..c2608d5fce82ca49a9ad2f2743fb4295a95ac8ad 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -1,64 +1,31 @@
# frozen_string_literal: true
-require 'flipper/adapters/active_record'
-require 'flipper/adapters/active_support_cache_store'
-
class Feature
prepend_if_ee('EE::Feature') # rubocop: disable Cop/InjectEnterpriseEditionModule
- # Classes to override flipper table names
- class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature
- # Using `self.table_name` won't work. ActiveRecord bug?
- superclass.table_name = 'features'
-
- def self.feature_names
- pluck(:key)
- end
- end
-
- class FlipperGate < Flipper::Adapters::ActiveRecord::Gate
- superclass.table_name = 'feature_gates'
- end
+ SUPPORTED_FEATURE_FLAG_ADAPTERS = %w[unleash flipper]
class << self
- delegate :group, to: :flipper
+ delegate :all, :get, :group, :persisted?, :table_exists?, to: :adapter
- def all
- flipper.features.to_a
- end
-
- def get(key)
- flipper.feature(key)
- end
-
- def persisted_names
- Gitlab::SafeRequestStore[:flipper_persisted_names] ||=
- begin
- # We saw on GitLab.com, this database request was called 2300
- # times/s. Let's cache it for a minute to avoid that load.
- Gitlab::ThreadMemoryCache.cache_backend.fetch('flipper:persisted_names', expires_in: 1.minute) do
- FlipperFeature.feature_names
- end
+ def adapter
+ @adapter ||=
+ SUPPORTED_FEATURE_FLAG_ADAPTERS.find do |type|
+ adapter = get_adapter(type)
+ break adapter if adapter.available?
end
end
- def persisted?(feature)
- # Flipper creates on-memory features when asked for a not-yet-created one.
- # If we want to check if a feature has been actually set, we look for it
- # on the persisted features list.
- persisted_names.include?(feature.name.to_s)
- end
-
# use `default_enabled: true` to default the flag to being `enabled`
# unless set explicitly. The default is `disabled`
def enabled?(key, thing = nil, default_enabled: false)
- feature = Feature.get(key)
+ feature = get(key)
# If we're not default enabling the flag or the feature has been set, always evaluate.
# `persisted?` can potentially generate DB queries and also checks for inclusion
# in an array of feature names (177 at last count), possibly reducing performance by half.
# So we only perform the `persisted` check if `default_enabled: true`
- !default_enabled || Feature.persisted?(feature) ? feature.enabled?(thing) : true
+ !default_enabled || persisted?(feature) ? feature.enabled?(thing) : true
end
def disabled?(key, thing = nil, default_enabled: false)
@@ -89,50 +56,10 @@ def remove(key)
feature.remove
end
- def flipper
- if Gitlab::SafeRequestStore.active?
- Gitlab::SafeRequestStore[:flipper] ||= build_flipper_instance
- else
- @flipper ||= build_flipper_instance
- end
- end
-
- def build_flipper_instance
- Flipper.new(flipper_adapter).tap { |flip| flip.memoize = true }
- end
-
- # This method is called from config/initializers/flipper.rb and can be used
- # to register Flipper groups.
- # See https://docs.gitlab.com/ee/development/feature_flags.html#feature-groups
- def register_feature_groups
- end
-
- def flipper_adapter
- active_record_adapter = Flipper::Adapters::ActiveRecord.new(
- feature_class: FlipperFeature,
- gate_class: FlipperGate)
-
- # Redis L2 cache
- redis_cache_adapter =
- Flipper::Adapters::ActiveSupportCacheStore.new(
- active_record_adapter,
- l2_cache_backend,
- expires_in: 1.hour)
-
- # Thread-local L1 cache: use a short timeout since we don't have a
- # way to expire this cache all at once
- Flipper::Adapters::ActiveSupportCacheStore.new(
- redis_cache_adapter,
- l1_cache_backend,
- expires_in: 1.minute)
- end
-
- def l1_cache_backend
- Gitlab::ThreadMemoryCache.cache_backend
- end
-
- def l2_cache_backend
- Rails.cache
+ private
+
+ def get_adapter(type)
+ "FeatureFlag::Adapters::#{type.camelize}".constantize
end
end
diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb
index 81f8ba5c8c3dd0efa7d83182c691009670648ba8..5351ccffe519a1a8ba19cab6462107b6943a48bc 100644
--- a/lib/feature/gitaly.rb
+++ b/lib/feature/gitaly.rb
@@ -16,7 +16,7 @@ class Gitaly
class << self
def enabled?(feature_flag)
- return false unless Feature::FlipperFeature.table_exists?
+ return false unless Feature.table_exists?
default_on = DEFAULT_ON_FLAGS.include?(feature_flag)
Feature.enabled?("gitaly_#{feature_flag}", default_enabled: default_on)
diff --git a/lib/feature_flag/adapters/flipper.rb b/lib/feature_flag/adapters/flipper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1f2d7fca3743b658a244a16936986fbe4cef1911
--- /dev/null
+++ b/lib/feature_flag/adapters/flipper.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require 'flipper/adapters/active_record'
+require 'flipper/adapters/active_support_cache_store'
+
+module FeatureFlag
+ module Adapters
+ class Flipper
+ # Classes to override flipper table names
+ class FlipperFeature < ::Flipper::Adapters::ActiveRecord::Feature
+ # Using `self.table_name` won't work. ActiveRecord bug?
+ superclass.table_name = 'features'
+
+ def self.feature_names
+ pluck(:key)
+ end
+ end
+
+ class FlipperGate < ::Flipper::Adapters::ActiveRecord::Gate
+ superclass.table_name = 'feature_gates'
+ end
+
+ class << self
+ delegate :group, to: :flipper
+
+ def available?
+ true
+ end
+
+ def all
+ flipper.features.to_a
+ end
+
+ def get(key)
+ flipper.feature(key)
+ end
+
+ # This method is called from config/initializers/flipper.rb and can be used
+ # to register Flipper groups.
+ # See https://docs.gitlab.com/ee/development/feature_flags.html#feature-groups
+ def register_feature_groups
+ end
+
+ def persisted?(feature)
+ # Flipper creates on-memory features when asked for a not-yet-created one.
+ # If we want to check if a feature has been actually set, we look for it
+ # on the persisted features list.
+ persisted_names.include?(feature.name.to_s)
+ end
+
+ def table_exists?
+ FlipperFeature.table_exists?
+ end
+
+ private
+
+ def flipper
+ if Gitlab::SafeRequestStore.active?
+ Gitlab::SafeRequestStore[:flipper] ||= build_flipper_instance
+ else
+ @flipper ||= build_flipper_instance
+ end
+ end
+
+ def persisted_names
+ Gitlab::SafeRequestStore[:flipper_persisted_names] ||=
+ begin
+ # We saw on GitLab.com, this database request was called 2300
+ # times/s. Let's cache it for a minute to avoid that load.
+ Gitlab::ThreadMemoryCache.cache_backend.fetch('flipper:persisted_names', expires_in: 1.minute) do
+ FlipperFeature.feature_names
+ end
+ end
+ end
+
+ def build_flipper_instance
+ ::Flipper.new(flipper_adapter).tap { |flip| flip.memoize = true }
+ end
+
+ def flipper_adapter
+ active_record_adapter = ::Flipper::Adapters::ActiveRecord.new(
+ feature_class: FlipperFeature,
+ gate_class: FlipperGate)
+
+ # Redis L2 cache
+ redis_cache_adapter =
+ ::Flipper::Adapters::ActiveSupportCacheStore.new(
+ active_record_adapter,
+ l2_cache_backend,
+ expires_in: 1.hour)
+
+ # Thread-local L1 cache: use a short timeout since we don't have a
+ # way to expire this cache all at once
+ ::Flipper::Adapters::ActiveSupportCacheStore.new(
+ redis_cache_adapter,
+ l1_cache_backend,
+ expires_in: 1.minute)
+ end
+
+ def l1_cache_backend
+ Gitlab::ThreadMemoryCache.cache_backend
+ end
+
+ def l2_cache_backend
+ Rails.cache
+ end
+ end
+ end
+ end
+end
diff --git a/lib/feature_flag/adapters/unleash.rb b/lib/feature_flag/adapters/unleash.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2d31fc3653658f2a039fbf39816e98c84f0ab16b
--- /dev/null
+++ b/lib/feature_flag/adapters/unleash.rb
@@ -0,0 +1,193 @@
+# frozen_string_literal: true
+
+module FeatureFlag
+ module Adapters
+ class Unleash
+ class Feature
+ attr_accessor :active
+ attr_accessor :strategies
+ attr_reader :name
+
+ alias_attribute :state, :active
+
+ Gate = Struct.new(:key, :value)
+
+ def initialize(name)
+ @name = name.to_s
+ end
+
+ def enabled?(thing = nil)
+ client.is_enabled?(name, context(thing))
+ end
+
+ def off?(thing = nil)
+ !enabled?(thing)
+ end
+
+ def enable(thing = true)
+ HTTParty.post(Unleash.enable_feature_flag_url(name),
+ headers: Unleash.request_headers,
+ body: { name: name,
+ environment_scope: Gitlab.config.unleash.app_name,
+ strategy: strategy_for(thing).to_json })
+ end
+
+ def disable(thing = false)
+ HTTParty.post(Unleash.disable_feature_flag_url(name),
+ headers: Unleash.request_headers,
+ body: { name: name,
+ environment_scope: Gitlab.config.unleash.app_name,
+ strategy: strategy_for(thing).to_json })
+ end
+
+ def enable_group(group)
+ # Not Supported yet (See https://gitlab.com/gitlab-org/gitlab-ee/issues/9566)
+ end
+
+ def disable_group(group)
+ # Not Supported yet (See https://gitlab.com/gitlab-org/gitlab-ee/issues/9566)
+ end
+
+ def remove
+ HTTParty.post(Unleash.delete_feature_flag_url(name),
+ headers: Unleash.request_headers,
+ body: { name: name,
+ environment_scope: Gitlab.config.unleash.app_name })
+ end
+
+ def persisted?
+ toggles = ::Unleash.toggles
+ toggles.present? && toggles.any? { |toggle| toggle['name'] == name }
+ end
+
+ def gates
+ @gates ||= strategies.map { |strategy| Gate.new(strategy['name'], strategy['parameters']) }
+ end
+
+ def gate_values
+ @gate_values ||= strategies.inject({}) do |hash, strategy|
+ hash[strategy['name']] = strategy['parameters'].to_s
+ hash
+ end
+ end
+
+ private
+
+ def strategy_for(thing)
+ if thing.in?([true, false])
+ { name: 'default', parameters: {} }
+ else
+ { name: 'userWithId', parameters: { userIds: sanitized(thing) } }
+ end
+ end
+
+ def client
+ FeatureFlag::Adapters::Unleash.client
+ end
+
+ def context(thing)
+ ::Unleash::Context.new(properties: { thing: sanitized(thing) })
+ end
+
+ def sanitized(thing)
+ thing = thing.__getobj__ if thing.respond_to?(:__getobj__) # Resolve SimpleDelegator
+
+ return thing unless thing.is_a?(ActiveRecord::Base)
+
+ "#{thing.class.name}:#{thing.id}"
+ end
+ end
+
+ class << self
+ include Gitlab::Utils::StrongMemoize
+
+ def available?
+ Gitlab.config.unleash.enabled
+ end
+
+ def all
+ response = HTTParty.get(get_feature_flag_scopes_url,
+ headers: request_headers,
+ query: { environment_scope: Gitlab.config.unleash.app_name }
+ )
+
+ response.map do |scope|
+ feature = Feature.new(scope['name'])
+ feature.active = scope['active']
+ feature.strategies = scope['strategies']
+ feature
+ end
+ end
+
+ def get(key)
+ Feature.new(key)
+ end
+
+ def persisted?(feature)
+ feature.persisted?
+ end
+
+ def table_exists?
+ true
+ end
+
+ def configure
+ ::Unleash.configure do |config|
+ config.url = Gitlab.config.unleash.url
+ config.app_name = Gitlab.config.unleash.app_name
+ config.instance_id = Gitlab.config.unleash.instance_id
+ config.logger = Gitlab::Unleash::Logger
+ end
+ end
+
+ def get_feature_flag_scopes_url
+ "#{api_endpoint}/feature_flag_scopes"
+ end
+
+ def enable_feature_flag_url(key)
+ "#{api_endpoint}/feature_flags/#{key}/enable"
+ end
+
+ def disable_feature_flag_url(key)
+ "#{api_endpoint}/feature_flags/#{key}/disable"
+ end
+
+ def delete_feature_flag_url(key)
+ "#{api_endpoint}/feature_flags/#{key}"
+ end
+
+ def api_endpoint
+ strong_memoize(:api_endpoint) do
+ api_url, project_id = Gitlab.config.unleash.url
+ .scan( %r{(https?://.*/api/v4)/feature_flags/unleash/(\d+)} )
+ .first
+
+ "#{api_url}/projects/#{project_id}/"
+ end
+ end
+
+ def request_headers
+ strong_memoize(:request_headers) do
+ { 'Private-Token': Gitlab.config.unleash.personal_access_token }
+ end
+ end
+
+ def client
+ # TODO: Fix
+ @client ||= if defined?(UNLEASH)
+ UNLEASH
+ elsif defined?(Rails.configuration.unleash)
+ Rails.configuration.unleash
+ else
+ ::Unleash::Client.new(
+ url: Gitlab.config.unleash.url,
+ app_name: Gitlab.config.unleash.app_name,
+ instance_id: Gitlab.config.unleash.instance_id,
+ logger: Gitlab::Unleash::Logger,
+ disable_metrics: true)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/unleash/logger.rb b/lib/gitlab/unleash/logger.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5de41d9abde4ee059fdbc1993e947b4088c31ba3
--- /dev/null
+++ b/lib/gitlab/unleash/logger.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Unleash
+ class Logger < ::Gitlab::JsonLogger
+ def self.file_name_noext
+ 'unleash'
+ end
+
+ def self.level=(level)
+ # no-op, otherwise `Unleash.logger.level` causes NotImplementedError.
+ end
+
+ def self.build
+ super.tap { |logger| logger.level = Rails.logger.level } # rubocop:disable Gitlab/RailsLogger
+ end
+ end
+ end
+end
diff --git a/lib/unleash/strategy/user_with_id.rb b/lib/unleash/strategy/user_with_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c9790f58e2d7989f1dce90dc6e54445d7c5b761a
--- /dev/null
+++ b/lib/unleash/strategy/user_with_id.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+##
+# We override the existing userWithId strategy to make it compatible with ActiveRecord
+# objects such as Project, Group, User, etc.
+#
+# To properly gate a feature per target, you have to define userWithId strategy
+# params in the following convention.
+#
+# ":"
+#
+# For example, if you want to enable a featue on the project (id: 123), you need
+# to define the value "Project:123".
+module Unleash
+ module Strategy
+ class UserWithId < Base
+ def name
+ 'userWithId'
+ end
+
+ # requires: params['userIds'], context.user_id,
+ def is_enabled?(params = {}, context = nil)
+ return false unless params.is_a?(Hash) && params.has_key?(PARAM)
+ return false unless params.fetch(PARAM, nil).is_a? String
+ return false unless context.class.name == 'Unleash::Context'
+
+ target = context.properties[:thing]
+
+ params[PARAM].split(",").map(&:strip).any? { |allowed| allowed == target }
+ end
+ end
+ end
+end