summaryrefslogtreecommitdiff
path: root/app/models/operations/feature_flags
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/operations/feature_flags')
-rw-r--r--app/models/operations/feature_flags/scope.rb13
-rw-r--r--app/models/operations/feature_flags/strategy.rb94
-rw-r--r--app/models/operations/feature_flags/strategy_user_list.rb12
-rw-r--r--app/models/operations/feature_flags/user_list.rb36
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