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
|
# frozen_string_literal: true
require 'active_support/core_ext/object/blank'
module QA
module Runtime
class Feature
SetFeatureError = Class.new(RuntimeError)
AuthorizationError = Class.new(RuntimeError)
UnknownScopeError = Class.new(RuntimeError)
UnknownStateError = Class.new(RuntimeError)
class << self
# Documentation: https://docs.gitlab.com/ee/api/features.html
include Support::API
def remove(key)
request = Runtime::API::Request.new(api_client, "/features/#{key}")
response = delete(request.url)
unless response.code == QA::Support::API::HTTP_STATUS_NO_CONTENT
raise SetFeatureError, "Deleting feature flag #{key} failed with `#{response}`."
end
end
def enable(key, **scopes)
set_and_verify(key, enable: true, **scopes)
end
def disable(key, **scopes)
set_and_verify(key, enable: false, **scopes)
end
# Set one or more flags to their specified state.
#
# @param [Hash] flags The feature flags and desired values, e.g., { 'flag1' => 'enabled', 'flag2' => "disabled" }
# @param [Hash] scopes The scope (user, project, group) to apply the feature flag to.
def set(flags, **scopes)
flags.each_pair do |flag, state|
case state
when 'enabled', 'enable', 'true', 1, true
enable(flag, **scopes)
when 'disabled', 'disable', 'false', 0, false
disable(flag, **scopes)
else
raise UnknownStateError, "Unknown feature flag state: #{state}"
end
end
end
def enabled?(key, **scopes)
feature = JSON.parse(get_features).find { |flag| flag['name'] == key.to_s }
feature && (feature['state'] == 'on' || feature['state'] == 'conditional' && scopes.present? && enabled_scope?(feature['gates'], **scopes))
end
private
def api_client
@api_client ||= Runtime::API::Client.as_admin
rescue Runtime::API::Client::AuthorizationError => e
raise AuthorizationError, "Administrator access is required to enable/disable feature flags. #{e.message}"
end
def enabled_scope?(gates, **scopes)
scopes.each do |key, value|
case key
when :project, :group, :user
actors = gates.find { |i| i['key'] == 'actors' }['value']
return actors.include?("#{key.to_s.capitalize}:#{value.id}")
when :feature_group
groups = gates.find { |i| i['key'] == 'groups' }['value']
return groups.include?(value)
end
end
raise UnknownScopeError, "Unknown scope in: #{scopes}"
end
def get_features
request = Runtime::API::Request.new(api_client, '/features')
response = get(request.url)
response.body
end
# Change a feature flag and verify that the change was successful
# Arguments:
# key: The feature flag to set (as a string)
# enable: `true` to enable the flag, `false` to disable it
# scopes: Any scope (user, project, group) to restrict the change to
def set_and_verify(key, enable:, **scopes)
msg = "#{enable ? 'En' : 'Dis'}abling feature: #{key}"
msg += " for scope \"#{scopes_to_s(**scopes)}\"" if scopes.present?
QA::Runtime::Logger.info(msg)
Support::Retrier.retry_on_exception(sleep_interval: 2) do
set_feature(key, enable, **scopes)
is_enabled = nil
QA::Support::Waiter.wait_until(sleep_interval: 1) do
is_enabled = enabled?(key, **scopes)
is_enabled == enable || !enable && scopes.present?
end
if is_enabled == enable
QA::Runtime::Logger.info("Successfully #{enable ? 'en' : 'dis'}abled and verified feature flag: #{key}")
else
raise SetFeatureError, "#{key} was not #{enable ? 'en' : 'dis'}abled!" if enable
QA::Runtime::Logger.warn("Feature flag scope was removed but the flag is still enabled globally.")
end
end
end
def set_feature(key, value, **scopes)
scopes[:project] = scopes[:project].full_path if scopes.key?(:project)
scopes[:group] = scopes[:group].full_path if scopes.key?(:group)
scopes[:user] = scopes[:user].username if scopes.key?(:user)
request = Runtime::API::Request.new(api_client, "/features/#{key}")
response = post(request.url, scopes.merge({ value: value }))
unless response.code == QA::Support::API::HTTP_STATUS_CREATED
raise SetFeatureError, "Setting feature flag #{key} to #{value} failed with `#{response}`."
end
end
def scopes_to_s(**scopes)
key = scopes.each_key.first
s = "#{key}: "
case key
when :project, :group
s += scopes[key].full_path
when :user
s += scopes[key].username
when :feature_group
s += scopes[key]
else
raise UnknownScopeError, "Unknown scope: #{key}"
end
s
end
end
end
end
end
|