diff options
Diffstat (limited to 'app/models/operations/feature_flags')
-rw-r--r-- | app/models/operations/feature_flags/scope.rb | 13 | ||||
-rw-r--r-- | app/models/operations/feature_flags/strategy.rb | 94 | ||||
-rw-r--r-- | app/models/operations/feature_flags/strategy_user_list.rb | 12 | ||||
-rw-r--r-- | app/models/operations/feature_flags/user_list.rb | 36 |
4 files changed, 155 insertions, 0 deletions
diff --git a/app/models/operations/feature_flags/scope.rb b/app/models/operations/feature_flags/scope.rb new file mode 100644 index 00000000000..d70101b5e0d --- /dev/null +++ b/app/models/operations/feature_flags/scope.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Operations + module FeatureFlags + class Scope < ApplicationRecord + prepend HasEnvironmentScope + + self.table_name = 'operations_scopes' + + belongs_to :strategy, class_name: 'Operations::FeatureFlags::Strategy' + end + end +end diff --git a/app/models/operations/feature_flags/strategy.rb b/app/models/operations/feature_flags/strategy.rb new file mode 100644 index 00000000000..ff68af9741e --- /dev/null +++ b/app/models/operations/feature_flags/strategy.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Operations + module FeatureFlags + class Strategy < ApplicationRecord + STRATEGY_DEFAULT = 'default' + STRATEGY_GITLABUSERLIST = 'gitlabUserList' + STRATEGY_GRADUALROLLOUTUSERID = 'gradualRolloutUserId' + STRATEGY_USERWITHID = 'userWithId' + STRATEGIES = { + STRATEGY_DEFAULT => [].freeze, + STRATEGY_GITLABUSERLIST => [].freeze, + STRATEGY_GRADUALROLLOUTUSERID => %w[groupId percentage].freeze, + STRATEGY_USERWITHID => ['userIds'].freeze + }.freeze + USERID_MAX_LENGTH = 256 + + self.table_name = 'operations_strategies' + + belongs_to :feature_flag + has_many :scopes, class_name: 'Operations::FeatureFlags::Scope' + has_one :strategy_user_list + has_one :user_list, through: :strategy_user_list + + validates :name, + inclusion: { + in: STRATEGIES.keys, + message: 'strategy name is invalid' + } + + validate :parameters_validations, if: -> { errors[:name].blank? } + validates :user_list, presence: true, if: -> { name == STRATEGY_GITLABUSERLIST } + validates :user_list, absence: true, if: -> { name != STRATEGY_GITLABUSERLIST } + validate :same_project_validation, if: -> { user_list.present? } + + accepts_nested_attributes_for :scopes, allow_destroy: true + + def user_list_id=(user_list_id) + self.user_list = ::Operations::FeatureFlags::UserList.find(user_list_id) + end + + private + + def same_project_validation + unless user_list.project_id == feature_flag.project_id + errors.add(:user_list, 'must belong to the same project') + end + end + + def parameters_validations + validate_parameters_type && + validate_parameters_keys && + validate_parameters_values + end + + def validate_parameters_type + parameters.is_a?(Hash) || parameters_error('parameters are invalid') + end + + def validate_parameters_keys + actual_keys = parameters.keys.sort + expected_keys = STRATEGIES[name].sort + expected_keys == actual_keys || parameters_error('parameters are invalid') + end + + def validate_parameters_values + case name + when STRATEGY_GRADUALROLLOUTUSERID + gradual_rollout_user_id_parameters_validation + when STRATEGY_USERWITHID + FeatureFlagUserXidsValidator.validate_user_xids(self, :parameters, parameters['userIds'], 'userIds') + end + end + + def gradual_rollout_user_id_parameters_validation + percentage = parameters['percentage'] + group_id = parameters['groupId'] + + unless percentage.is_a?(String) && percentage.match(/\A[1-9]?[0-9]\z|\A100\z/) + parameters_error('percentage must be a string between 0 and 100 inclusive') + end + + unless group_id.is_a?(String) && group_id.match(/\A[a-z]{1,32}\z/) + parameters_error('groupId parameter is invalid') + end + end + + def parameters_error(message) + errors.add(:parameters, message) + false + end + end + end +end diff --git a/app/models/operations/feature_flags/strategy_user_list.rb b/app/models/operations/feature_flags/strategy_user_list.rb new file mode 100644 index 00000000000..813b632dd67 --- /dev/null +++ b/app/models/operations/feature_flags/strategy_user_list.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Operations + module FeatureFlags + class StrategyUserList < ApplicationRecord + self.table_name = 'operations_strategies_user_lists' + + belongs_to :strategy + belongs_to :user_list + end + end +end diff --git a/app/models/operations/feature_flags/user_list.rb b/app/models/operations/feature_flags/user_list.rb new file mode 100644 index 00000000000..b9bdcb59d5f --- /dev/null +++ b/app/models/operations/feature_flags/user_list.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Operations + module FeatureFlags + class UserList < ApplicationRecord + include AtomicInternalId + include IidRoutes + + self.table_name = 'operations_user_lists' + + belongs_to :project + has_many :strategy_user_lists + has_many :strategies, through: :strategy_user_lists + + has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.operations_feature_flags_user_lists&.maximum(:iid) }, presence: true + + validates :project, presence: true + validates :name, + presence: true, + uniqueness: { scope: :project_id }, + length: 1..255 + validates :user_xids, feature_flag_user_xids: true + + before_destroy :ensure_no_associated_strategies + + private + + def ensure_no_associated_strategies + if strategies.present? + errors.add(:base, 'User list is associated with a strategy') + throw :abort # rubocop: disable Cop/BanCatchThrow + end + end + end + end +end |