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
|
# frozen_string_literal: true
require 'active_support/core_ext/object/blank'
module QA
module Runtime
class Feature
class << self
# Documentation: https://docs.gitlab.com/ee/api/features.html
include Support::Api
SetFeatureError = Class.new(RuntimeError)
AuthorizationError = Class.new(RuntimeError)
UnknownScopeError = Class.new(RuntimeError)
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
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.filter { |i| i['key'] == 'actors' }.first['value']
break actors.include?("#{key.to_s.capitalize}:#{value.id}")
when :feature_group
groups = gates.filter { |i| i['key'] == 'groups' }.first['value']
break groups.include?(value)
else
raise UnknownScopeError, "Unknown scope: #{key}"
end
end
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
|