summaryrefslogtreecommitdiff
path: root/lib/api/features.rb
blob: 9142591aebd7b24b6e055b93156f3c233d6e9758 (plain)
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')