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')
|