diff options
Diffstat (limited to 'app/services/feature_flags')
-rw-r--r-- | app/services/feature_flags/base_service.rb | 55 | ||||
-rw-r--r-- | app/services/feature_flags/create_service.rb | 52 | ||||
-rw-r--r-- | app/services/feature_flags/destroy_service.rb | 33 | ||||
-rw-r--r-- | app/services/feature_flags/disable_service.rb | 46 | ||||
-rw-r--r-- | app/services/feature_flags/enable_service.rb | 93 | ||||
-rw-r--r-- | app/services/feature_flags/update_service.rb | 87 |
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 |