diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb index 01db0a5cf8b23843634e1e05a5275f0a13db1261..b93537e0d1edf376b7d774339ae7709a121bb55d 100644 --- a/app/models/operations/feature_flag.rb +++ b/app/models/operations/feature_flag.rb @@ -52,7 +52,7 @@ class FeatureFlag < ApplicationRecord class << self def preload_relations - preload(strategies: :scopes) + preload(strategies: [:scopes, :user_list]) end def for_unleash_client(project, environment) diff --git a/doc/api/feature_flags.md b/doc/api/feature_flags.md index d7bf4fe826e53404190bceeba753a509badaeb78..06661207f8eaf70efda5f41901f65bfcf8d5d89e 100644 --- a/doc/api/feature_flags.md +++ b/doc/api/feature_flags.md @@ -59,7 +59,8 @@ Example response: "id": 1, "environment_scope": "production" } - ] + ], + "user_list": null } ] }, @@ -81,7 +82,36 @@ Example response: "id": 2, "environment_scope": "staging" } - ] + ], + "user_list": null + } + ] + }, + { + "name":"user_list", + "description":"This feature is about user list", + "active": true, + "version": "new_version_flag", + "created_at":"2019-11-04T08:13:10.507Z", + "updated_at":"2019-11-04T08:13:10.507Z", + "scopes":[], + "strategies": [ + { + "id": 2, + "name": "gitlabUserList", + "parameters": {}, + "scopes": [ + { + "id": 2, + "environment_scope": "staging" + } + ], + "user_list": { + "id": 1, + "iid": 1, + "name": "My user list", + "user_xids": "user1,user2,user3" + } } ] } @@ -126,7 +156,8 @@ Example response: "id": 37, "environment_scope": "production" } - ] + ], + "user_list": null } ] } @@ -152,6 +183,7 @@ POST /projects/:id/feature_flags | `strategies:parameters` | JSON | no | The strategy parameters. | | `strategies:scopes` | JSON | no | The scopes for the strategy. | | `strategies:scopes:environment_scope` | string | no | The environment scope of the scope. | +| `strategies:user_list_id` | integer/string | no | The ID of the feature flag user list. If strategy is `gitlabUserList`. | ```shell curl "https://gitlab.example.com/api/v4/projects/1/feature_flags" \ @@ -217,6 +249,7 @@ PUT /projects/:id/feature_flags/:feature_flag_name | `strategies:scopes:id` | JSON | no | The environment scope ID. | | `strategies:scopes:environment_scope` | string | no | The environment scope of the scope. | | `strategies:scopes:_destroy` | boolean | no | Delete the scope when true. | +| `strategies:user_list_id` | integer/string | no | The ID of the feature flag user list. If strategy is `gitlabUserList`. | ```shell curl "https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature" \ diff --git a/lib/api/entities/feature_flag/basic_user_list.rb b/lib/api/entities/feature_flag/basic_user_list.rb new file mode 100644 index 0000000000000000000000000000000000000000..df577e9f1a4f855c77e0db05640a896ebdfadfd1 --- /dev/null +++ b/lib/api/entities/feature_flag/basic_user_list.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class FeatureFlag < Grape::Entity + class BasicUserList < Grape::Entity + expose :id, documentation: { type: 'integer', example: 1 } + expose :iid, documentation: { type: 'integer', example: 1 } + expose :name, documentation: { type: 'string', example: 'user_list' } + expose :user_xids, documentation: { type: 'string', example: 'user1,user2' } + end + end + end +end diff --git a/lib/api/entities/feature_flag/strategy.rb b/lib/api/entities/feature_flag/strategy.rb index 62178420370256b6cbde90acba3336951c7d3184..aea38f0e24a6132596188d6a108e86e913906a6e 100644 --- a/lib/api/entities/feature_flag/strategy.rb +++ b/lib/api/entities/feature_flag/strategy.rb @@ -8,6 +8,7 @@ class Strategy < Grape::Entity expose :name, documentation: { type: 'string', example: 'userWithId' } expose :parameters, documentation: { type: 'string', example: '{"userIds": "user1"}' } expose :scopes, using: FeatureFlag::Scope + expose :user_list, using: FeatureFlag::BasicUserList end end end diff --git a/lib/api/entities/feature_flag/user_list.rb b/lib/api/entities/feature_flag/user_list.rb index efb3261658ac73daa56f7ce6c405380dd1b933d8..47f89cea4d20bdd08e56e4f875a850d5ef51d889 100644 --- a/lib/api/entities/feature_flag/user_list.rb +++ b/lib/api/entities/feature_flag/user_list.rb @@ -3,16 +3,12 @@ module API module Entities class FeatureFlag < Grape::Entity - class UserList < Grape::Entity + class UserList < BasicUserList include RequestAwareEntity - expose :id, documentation: { type: 'integer', example: 1 } - expose :iid, documentation: { type: 'integer', example: 1 } expose :project_id, documentation: { type: 'integer', example: 2 } expose :created_at, documentation: { type: 'dateTime', example: '2020-02-04T08:13:10.507Z' } expose :updated_at, documentation: { type: 'dateTime', example: '2020-02-04T08:13:10.507Z' } - expose :name, documentation: { type: 'string', example: 'user_list' } - expose :user_xids, documentation: { type: 'string', example: 'user1,user2' } expose :path do |list| project_feature_flags_user_list_path(list.project, list) diff --git a/lib/api/feature_flags.rb b/lib/api/feature_flags.rb index 1846ddf6833221d76dc3465effe9073876a30e3b..4ed288ee997bd116ec7ea4cc13c23d388d46dffe 100644 --- a/lib/api/feature_flags.rb +++ b/lib/api/feature_flags.rb @@ -63,7 +63,8 @@ class FeatureFlags < ::API::Base optional :version, type: String, desc: 'The version of the feature flag. Must be `new_version_flag`. Omit to create a Legacy feature flag.' optional :strategies, type: Array do requires :name, type: String, desc: 'The strategy name. Can be `default`, `gradualRolloutUserId`, `userWithId`, or `gitlabUserList`. In GitLab 13.5 and later, can be `flexibleRollout`' - requires :parameters, type: JSON, desc: 'The strategy parameters as a JSON-formatted string e.g. `{"userIds":"user1"}`', documentation: { type: 'String' } + optional :parameters, type: JSON, desc: 'The strategy parameters as a JSON-formatted string e.g. `{"userIds":"user1"}`', documentation: { type: 'String' } + optional :user_list_id, type: Integer, desc: "The ID of the feature flag user list. If strategy is `gitlabUserList`." optional :scopes, type: Array do requires :environment_scope, type: String, desc: 'The environment scope of the scope' end @@ -131,6 +132,7 @@ class FeatureFlags < ::API::Base optional :id, type: Integer, desc: 'The feature flag strategy ID' optional :name, type: String, desc: 'The strategy name' optional :parameters, type: JSON, desc: 'The strategy parameters as a JSON-formatted string e.g. `{"userIds":"user1"}`', documentation: { type: 'String' } + optional :user_list_id, type: Integer, desc: "The ID of the feature flag user list" optional :_destroy, type: Boolean, desc: 'Delete the strategy when true' optional :scopes, type: Array do optional :id, type: Integer, desc: 'The scope id' diff --git a/spec/fixtures/api/schemas/public_api/v4/operations/strategy.json b/spec/fixtures/api/schemas/public_api/v4/operations/strategy.json index f572b1a4f9bbb04b6b492427a6272c49300c08fc..a72260af1453923d24bc027a6f5ffe0fee1a9e86 100644 --- a/spec/fixtures/api/schemas/public_api/v4/operations/strategy.json +++ b/spec/fixtures/api/schemas/public_api/v4/operations/strategy.json @@ -8,7 +8,8 @@ "id": { "type": "integer" }, "name": { "type": "string" }, "parameters": { "type": "object" }, - "scopes": { "type": "array", "items": { "$ref": "scope.json" } } + "scopes": { "type": "array", "items": { "$ref": "scope.json" } }, + "user_list": { "type": ["object", "null"], "$ref": "user_list.json" } }, "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/public_api/v4/operations/user_list.json b/spec/fixtures/api/schemas/public_api/v4/operations/user_list.json new file mode 100644 index 0000000000000000000000000000000000000000..6a9f977e37de65861c987bd3eb2475a84acdea1c --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/operations/user_list.json @@ -0,0 +1,16 @@ +{ + "type": ["object", "null"], + "required": [ + "id", + "iid", + "name", + "user_xids" + ], + "properties": { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "name": { "type": "string" }, + "user_xids": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/requests/api/feature_flags_spec.rb b/spec/requests/api/feature_flags_spec.rb index 69e3633de577e0882dfb12dfecf5be3b268af1b3..4fb0dfbb0704e69bffa7a148032dc20989b44cf4 100644 --- a/spec/requests/api/feature_flags_spec.rb +++ b/spec/requests/api/feature_flags_spec.rb @@ -111,7 +111,57 @@ 'scopes' => [{ 'id' => scope.id, 'environment_scope' => 'production' - }] + }], + 'user_list' => nil + }] + }]) + end + end + + context 'with user_list strategy feature flags' do + let!(:feature_flag) do + create(:operations_feature_flag, :new_version_flag, project: project, name: 'feature1') + end + + let!(:user_list) do + create(:operations_feature_flag_user_list, project: project) + end + + let!(:strategy) do + create(:operations_strategy, :gitlab_userlist, user_list: user_list, feature_flag: feature_flag, name: 'gitlabUserList', parameters: {}) + end + + let!(:scope) do + create(:operations_scope, strategy: strategy, environment_scope: 'production') + end + + it 'returns the feature flags', :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/feature_flags') + expect(json_response).to eq([{ + 'name' => 'feature1', + 'description' => nil, + 'active' => true, + 'version' => 'new_version_flag', + 'updated_at' => feature_flag.updated_at.as_json, + 'created_at' => feature_flag.created_at.as_json, + 'scopes' => [], + 'strategies' => [{ + 'id' => strategy.id, + 'name' => 'gitlabUserList', + 'parameters' => {}, + 'scopes' => [{ + 'id' => scope.id, + 'environment_scope' => 'production' + }], + 'user_list' => { + 'id' => user_list.id, + 'iid' => user_list.iid, + 'name' => user_list.name, + 'user_xids' => user_list.user_xids + } }] }]) end @@ -162,7 +212,57 @@ 'scopes' => [{ 'id' => scope.id, 'environment_scope' => 'production' - }] + }], + 'user_list' => nil + }] + }) + end + end + + context 'with user_list strategy feature flag' do + let!(:feature_flag) do + create(:operations_feature_flag, :new_version_flag, project: project, name: 'feature1') + end + + let(:user_list) do + create(:operations_feature_flag_user_list, project: project) + end + + let!(:strategy) do + create(:operations_strategy, :gitlab_userlist, user_list: user_list, feature_flag: feature_flag, name: 'gitlabUserList', parameters: {}) + end + + let!(:scope) do + create(:operations_scope, strategy: strategy, environment_scope: 'production') + end + + it 'returns the feature flag', :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/feature_flag') + expect(json_response).to eq({ + 'name' => 'feature1', + 'description' => nil, + 'active' => true, + 'version' => 'new_version_flag', + 'updated_at' => feature_flag.updated_at.as_json, + 'created_at' => feature_flag.created_at.as_json, + 'scopes' => [], + 'strategies' => [{ + 'id' => strategy.id, + 'name' => 'gitlabUserList', + 'parameters' => {}, + 'scopes' => [{ + 'id' => scope.id, + 'environment_scope' => 'production' + }], + 'user_list' => { + 'id' => user_list.id, + 'iid' => user_list.iid, + 'name' => user_list.name, + 'user_xids' => user_list.user_xids + } }] }) end @@ -224,6 +324,10 @@ end context 'when creating a version 2 feature flag' do + let(:user_list) do + create(:operations_feature_flag_user_list, project: project) + end + it 'creates a new feature flag' do params = { name: 'new-feature', @@ -348,6 +452,32 @@ environment_scope: 'staging' }]) end + + it 'creates a new feature flag with user list strategy', :aggregate_failures do + params = { + name: 'new-feature', + version: 'new_version_flag', + strategies: [{ + name: 'gitlabUserList', + parameters: {}, + user_list_id: user_list.id + }] + } + + post api("/projects/#{project.id}/feature_flags", user), params: params + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema('public_api/v4/feature_flag') + + feature_flag = project.operations_feature_flags.last + expect(feature_flag.name).to eq(params[:name]) + expect(feature_flag.version).to eq('new_version_flag') + expect(feature_flag.strategies.map { |s| s.slice(:name, :parameters).deep_symbolize_keys }).to eq([{ + name: 'gitlabUserList', + parameters: {} + }]) + expect(feature_flag.strategies.first.user_list).to eq(user_list) + end end context 'when given invalid parameters' do @@ -369,6 +499,10 @@ project: project, active: true, name: 'feature1', description: 'old description') end + let(:user_list) do + create(:operations_feature_flag_user_list, project: project) + end + it 'returns a 404 if the feature flag does not exist' do params = { description: 'new description' } @@ -537,6 +671,30 @@ }]) end + it 'updates an existing feature flag strategy to be gitlab user list strategy', :aggregate_failures do + strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {}) + params = { + strategies: [{ + id: strategy.id, + name: 'gitlabUserList', + user_list_id: user_list.id, + parameters: {} + }] + } + + put api("/projects/#{project.id}/feature_flags/feature1", user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/feature_flag') + result = feature_flag.reload.strategies.map { |s| s.slice(:id, :name, :parameters).deep_symbolize_keys } + expect(result).to eq([{ + id: strategy.id, + name: 'gitlabUserList', + parameters: {} + }]) + expect(feature_flag.strategies.first.user_list).to eq(user_list) + end + it 'adds a new gradual rollout strategy to a feature flag' do strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'default', parameters: {}) params = {