Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/services/feature_flags')
-rw-r--r--app/services/feature_flags/base_service.rb55
-rw-r--r--app/services/feature_flags/create_service.rb52
-rw-r--r--app/services/feature_flags/destroy_service.rb33
-rw-r--r--app/services/feature_flags/disable_service.rb46
-rw-r--r--app/services/feature_flags/enable_service.rb93
-rw-r--r--app/services/feature_flags/update_service.rb87
6 files changed, 366 insertions, 0 deletions
diff --git a/app/services/feature_flags/base_service.rb b/app/services/feature_flags/base_service.rb
new file mode 100644
index 00000000000..9b27df90992
--- /dev/null
+++ b/app/services/feature_flags/base_service.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module FeatureFlags
+ class BaseService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ AUDITABLE_ATTRIBUTES = %w(name description active).freeze
+
+ protected
+
+ def audit_event(feature_flag)
+ message = audit_message(feature_flag)
+
+ return if message.blank?
+
+ details =
+ {
+ custom_message: message,
+ target_id: feature_flag.id,
+ target_type: feature_flag.class.name,
+ target_details: feature_flag.name
+ }
+
+ ::AuditEventService.new(
+ current_user,
+ feature_flag.project,
+ details
+ )
+ end
+
+ def save_audit_event(audit_event)
+ return unless audit_event
+
+ audit_event.security_event
+ end
+
+ def created_scope_message(scope)
+ "Created rule <strong>#{scope.environment_scope}</strong> "\
+ "and set it as <strong>#{scope.active ? "active" : "inactive"}</strong> "\
+ "with strategies <strong>#{scope.strategies}</strong>."
+ end
+
+ def feature_flag_by_name
+ strong_memoize(:feature_flag_by_name) do
+ project.operations_feature_flags.find_by_name(params[:name])
+ end
+ end
+
+ def feature_flag_scope_by_environment_scope
+ strong_memoize(:feature_flag_scope_by_environment_scope) do
+ feature_flag_by_name.scopes.find_by_environment_scope(params[:environment_scope])
+ end
+ end
+ end
+end
diff --git a/app/services/feature_flags/create_service.rb b/app/services/feature_flags/create_service.rb
new file mode 100644
index 00000000000..b4ca90f7aae
--- /dev/null
+++ b/app/services/feature_flags/create_service.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module FeatureFlags
+ class CreateService < FeatureFlags::BaseService
+ def execute
+ return error('Access Denied', 403) unless can_create?
+ return error('Version is invalid', :bad_request) unless valid_version?
+ return error('New version feature flags are not enabled for this project', :bad_request) unless flag_version_enabled?
+
+ ActiveRecord::Base.transaction do
+ feature_flag = project.operations_feature_flags.new(params)
+
+ if feature_flag.save
+ save_audit_event(audit_event(feature_flag))
+
+ success(feature_flag: feature_flag)
+ else
+ error(feature_flag.errors.full_messages, 400)
+ end
+ end
+ end
+
+ private
+
+ def audit_message(feature_flag)
+ message_parts = ["Created feature flag <strong>#{feature_flag.name}</strong>",
+ "with description <strong>\"#{feature_flag.description}\"</strong>."]
+
+ message_parts += feature_flag.scopes.map do |scope|
+ created_scope_message(scope)
+ end
+
+ message_parts.join(" ")
+ end
+
+ def can_create?
+ Ability.allowed?(current_user, :create_feature_flag, project)
+ end
+
+ def valid_version?
+ !params.key?(:version) || Operations::FeatureFlag.versions.key?(params[:version])
+ end
+
+ def flag_version_enabled?
+ params[:version] != 'new_version_flag' || new_version_feature_flags_enabled?
+ end
+
+ def new_version_feature_flags_enabled?
+ ::Feature.enabled?(:feature_flags_new_version, project, default_enabled: true)
+ end
+ end
+end
diff --git a/app/services/feature_flags/destroy_service.rb b/app/services/feature_flags/destroy_service.rb
new file mode 100644
index 00000000000..c77e3e03ec3
--- /dev/null
+++ b/app/services/feature_flags/destroy_service.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module FeatureFlags
+ class DestroyService < FeatureFlags::BaseService
+ def execute(feature_flag)
+ destroy_feature_flag(feature_flag)
+ end
+
+ private
+
+ def destroy_feature_flag(feature_flag)
+ return error('Access Denied', 403) unless can_destroy?(feature_flag)
+
+ ActiveRecord::Base.transaction do
+ if feature_flag.destroy
+ save_audit_event(audit_event(feature_flag))
+
+ success(feature_flag: feature_flag)
+ else
+ error(feature_flag.errors.full_messages)
+ end
+ end
+ end
+
+ def audit_message(feature_flag)
+ "Deleted feature flag <strong>#{feature_flag.name}</strong>."
+ end
+
+ def can_destroy?(feature_flag)
+ Ability.allowed?(current_user, :destroy_feature_flag, feature_flag)
+ end
+ end
+end
diff --git a/app/services/feature_flags/disable_service.rb b/app/services/feature_flags/disable_service.rb
new file mode 100644
index 00000000000..8a443ac1795
--- /dev/null
+++ b/app/services/feature_flags/disable_service.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module FeatureFlags
+ class DisableService < BaseService
+ def execute
+ return error('Feature Flag not found', 404) unless feature_flag_by_name
+ return error('Feature Flag Scope not found', 404) unless feature_flag_scope_by_environment_scope
+ return error('Strategy not found', 404) unless strategy_exist_in_persisted_data?
+
+ ::FeatureFlags::UpdateService
+ .new(project, current_user, update_params)
+ .execute(feature_flag_by_name)
+ end
+
+ private
+
+ def update_params
+ if remaining_strategies.empty?
+ params_to_destroy_scope
+ else
+ params_to_update_scope
+ end
+ end
+
+ def remaining_strategies
+ strong_memoize(:remaining_strategies) do
+ feature_flag_scope_by_environment_scope.strategies.reject do |strategy|
+ strategy['name'] == params[:strategy]['name'] &&
+ strategy['parameters'] == params[:strategy]['parameters']
+ end
+ end
+ end
+
+ def strategy_exist_in_persisted_data?
+ feature_flag_scope_by_environment_scope.strategies != remaining_strategies
+ end
+
+ def params_to_destroy_scope
+ { scopes_attributes: [{ id: feature_flag_scope_by_environment_scope.id, _destroy: true }] }
+ end
+
+ def params_to_update_scope
+ { scopes_attributes: [{ id: feature_flag_scope_by_environment_scope.id, strategies: remaining_strategies }] }
+ end
+ end
+end
diff --git a/app/services/feature_flags/enable_service.rb b/app/services/feature_flags/enable_service.rb
new file mode 100644
index 00000000000..b4cbb32e003
--- /dev/null
+++ b/app/services/feature_flags/enable_service.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module FeatureFlags
+ class EnableService < BaseService
+ def execute
+ if feature_flag_by_name
+ update_feature_flag
+ else
+ create_feature_flag
+ end
+ end
+
+ private
+
+ def create_feature_flag
+ ::FeatureFlags::CreateService
+ .new(project, current_user, create_params)
+ .execute
+ end
+
+ def update_feature_flag
+ ::FeatureFlags::UpdateService
+ .new(project, current_user, update_params)
+ .execute(feature_flag_by_name)
+ end
+
+ def create_params
+ if params[:environment_scope] == '*'
+ params_to_create_flag_with_default_scope
+ else
+ params_to_create_flag_with_additional_scope
+ end
+ end
+
+ def update_params
+ if feature_flag_scope_by_environment_scope
+ params_to_update_scope
+ else
+ params_to_create_scope
+ end
+ end
+
+ def params_to_create_flag_with_default_scope
+ {
+ name: params[:name],
+ scopes_attributes: [
+ {
+ active: true,
+ environment_scope: '*',
+ strategies: [params[:strategy]]
+ }
+ ]
+ }
+ end
+
+ def params_to_create_flag_with_additional_scope
+ {
+ name: params[:name],
+ scopes_attributes: [
+ {
+ active: false,
+ environment_scope: '*'
+ },
+ {
+ active: true,
+ environment_scope: params[:environment_scope],
+ strategies: [params[:strategy]]
+ }
+ ]
+ }
+ end
+
+ def params_to_create_scope
+ {
+ scopes_attributes: [{
+ active: true,
+ environment_scope: params[:environment_scope],
+ strategies: [params[:strategy]]
+ }]
+ }
+ end
+
+ def params_to_update_scope
+ {
+ scopes_attributes: [{
+ id: feature_flag_scope_by_environment_scope.id,
+ active: true,
+ strategies: feature_flag_scope_by_environment_scope.strategies | [params[:strategy]]
+ }]
+ }
+ end
+ end
+end
diff --git a/app/services/feature_flags/update_service.rb b/app/services/feature_flags/update_service.rb
new file mode 100644
index 00000000000..c837e50b104
--- /dev/null
+++ b/app/services/feature_flags/update_service.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module FeatureFlags
+ class UpdateService < FeatureFlags::BaseService
+ AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES = {
+ 'active' => 'active state',
+ 'environment_scope' => 'environment scope',
+ 'strategies' => 'strategies'
+ }.freeze
+
+ def execute(feature_flag)
+ return error('Access Denied', 403) unless can_update?(feature_flag)
+
+ ActiveRecord::Base.transaction do
+ feature_flag.assign_attributes(params)
+
+ feature_flag.strategies.each do |strategy|
+ if strategy.name_changed? && strategy.name_was == ::Operations::FeatureFlags::Strategy::STRATEGY_GITLABUSERLIST
+ strategy.user_list = nil
+ end
+ end
+
+ audit_event = audit_event(feature_flag)
+
+ if feature_flag.save
+ save_audit_event(audit_event)
+
+ success(feature_flag: feature_flag)
+ else
+ error(feature_flag.errors.full_messages, :bad_request)
+ end
+ end
+ end
+
+ private
+
+ def audit_message(feature_flag)
+ changes = changed_attributes_messages(feature_flag)
+ changes += changed_scopes_messages(feature_flag)
+
+ return if changes.empty?
+
+ "Updated feature flag <strong>#{feature_flag.name}</strong>. " + changes.join(" ")
+ end
+
+ def changed_attributes_messages(feature_flag)
+ feature_flag.changes.slice(*AUDITABLE_ATTRIBUTES).map do |attribute_name, changes|
+ "Updated #{attribute_name} "\
+ "from <strong>\"#{changes.first}\"</strong> to "\
+ "<strong>\"#{changes.second}\"</strong>."
+ end
+ end
+
+ def changed_scopes_messages(feature_flag)
+ feature_flag.scopes.map do |scope|
+ if scope.new_record?
+ created_scope_message(scope)
+ elsif scope.marked_for_destruction?
+ deleted_scope_message(scope)
+ else
+ updated_scope_message(scope)
+ end
+ end.compact # updated_scope_message can return nil if nothing has been changed
+ end
+
+ def deleted_scope_message(scope)
+ "Deleted rule <strong>#{scope.environment_scope}</strong>."
+ end
+
+ def updated_scope_message(scope)
+ changes = scope.changes.slice(*AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES.keys)
+ return if changes.empty?
+
+ message = "Updated rule <strong>#{scope.environment_scope}</strong> "
+ message += changes.map do |attribute_name, change|
+ name = AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES[attribute_name]
+ "#{name} from <strong>#{change.first}</strong> to <strong>#{change.second}</strong>"
+ end.join(' ')
+
+ message + '.'
+ end
+
+ def can_update?(feature_flag)
+ Ability.allowed?(current_user, :update_feature_flag, feature_flag)
+ end
+ end
+end