1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
|
# frozen_string_literal: true
module API
class Features < ::API::Base
before { authenticated_as_admin! }
features_tags = %w[features]
feature_category :feature_flags
urgency :low
BadValueError = Class.new(StandardError)
# TODO: remove these helpers with feature flag set_feature_flag_service
helpers do
def gate_value(params)
case params[:value]
when 'true'
true
when '0', 'false'
false
else
raise BadValueError unless params[:value].match?(/^\d+(\.\d+)?$/)
# https://github.com/jnunemaker/flipper/blob/master/lib/flipper/typecast.rb#L47
if params[:value].to_s.include?('.')
params[:value].to_f
else
params[:value].to_i
end
end
end
def gate_key(params)
case params[:key]
when 'percentage_of_actors'
:percentage_of_actors
else
:percentage_of_time
end
end
def gate_targets(params)
Feature::Target.new(params).targets
end
def gate_specified?(params)
Feature::Target.new(params).gate_specified?
end
end
resource :features do
desc 'List all features' do
detail 'Get a list of all persisted features, with its gate values.'
success Entities::Feature
is_array true
tags features_tags
end
get do
features = Feature.all
present features, with: Entities::Feature, current_user: current_user
end
desc 'List all feature definitions' do
detail 'Get a list of all feature definitions.'
success Entities::Feature::Definition
is_array true
tags features_tags
end
get :definitions do
definitions = ::Feature::Definition.definitions.values.map(&:to_h)
present definitions, with: Entities::Feature::Definition, current_user: current_user
end
desc 'Set or create a feature' do
detail "Set a feature's gate value. If a feature with the given name doesn't exist yet, it's created. " \
"The value can be a boolean, or an integer to indicate percentage of time."
success Entities::Feature
failure [
{ code: 400, message: 'Bad request' }
]
tags features_tags
end
params do
requires :value,
types: [String, Integer],
desc: '`true` or `false` to enable/disable, or an integer for percentage of time'
optional :key, type: String, desc: '`percentage_of_actors` or `percentage_of_time` (default)'
optional :feature_group, type: String, desc: 'A Feature group name'
optional :user, type: String, desc: 'A GitLab username or comma-separated multiple usernames'
optional :group,
type: String,
desc: "A GitLab group's path, for example `gitlab-org`, or comma-separated multiple group paths"
optional :namespace,
type: String,
desc: "A GitLab group or user namespace's path, for example `john-doe`, or comma-separated " \
"multiple namespace paths. Introduced in GitLab 15.0."
optional :project,
type: String,
desc: "A projects path, for example `gitlab-org/gitlab-foss`, or comma-separated multiple project paths"
optional :repository,
type: String,
desc: "A repository path, for example `gitlab-org/gitlab-test.git`, `gitlab-org/gitlab-test.wiki.git`, " \
"`snippets/21.git`, to name a few. Use comma to separate multiple repository paths"
optional :force, type: Boolean, desc: 'Skip feature flag validation checks, such as a YAML definition'
mutually_exclusive :key, :feature_group
mutually_exclusive :key, :user
mutually_exclusive :key, :group
mutually_exclusive :key, :namespace
mutually_exclusive :key, :project
mutually_exclusive :key, :repository
end
post ':name' do
if Feature.enabled?(:set_feature_flag_service)
flag_params = declared_params(include_missing: false)
response = ::Admin::SetFeatureFlagService
.new(feature_flag_name: params[:name], params: flag_params)
.execute
if response.success?
present response.payload[:feature_flag],
with: Entities::Feature, current_user: current_user
else
bad_request!(response.message)
end
else
validate_feature_flag_name!(params[:name]) unless params[:force]
targets = gate_targets(params)
value = gate_value(params)
key = gate_key(params)
case value
when true
if gate_specified?(params)
targets.each { |target| Feature.enable(params[:name], target) }
else
Feature.enable(params[:name])
end
when false
if gate_specified?(params)
targets.each { |target| Feature.disable(params[:name], target) }
else
Feature.disable(params[:name])
end
else
if key == :percentage_of_actors
Feature.enable_percentage_of_actors(params[:name], value)
else
Feature.enable_percentage_of_time(params[:name], value)
end
end
present Feature.get(params[:name]), # rubocop:disable Gitlab/AvoidFeatureGet
with: Entities::Feature, current_user: current_user
end
rescue BadValueError
bad_request!("Value must be boolean or numeric, got #{params[:value]}")
rescue Feature::Target::UnknownTargetError => e
bad_request!(e.message)
end
desc 'Delete a feature' do
detail "Removes a feature gate. Response is equal when the gate exists, or doesn't."
tags features_tags
end
delete ':name' do
Feature.remove(params[:name])
no_content!
end
end
# TODO: remove this helper with feature flag set_feature_flag_service
helpers do
def validate_feature_flag_name!(name)
# no-op
end
end
end
end
API::Features.prepend_mod_with('API::Features')
|