1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +01:00

feat: Add support for toggle types (#618)

This commit is contained in:
Ivar Conradi Østhus 2020-08-06 11:18:52 +02:00 committed by GitHub
parent 045a2dc621
commit 6568457ed8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 258 additions and 32 deletions

View File

@ -18,6 +18,7 @@ This endpoint is the one all admin ui should use to fetch all available feature
{
"name": "Feature.A",
"description": "lorem ipsum",
"type": "release",
"enabled": false,
"strategies": [
{
@ -68,6 +69,7 @@ Used to fetch details about a specific featureToggle. This is mostly provded to
{
"name": "Feature.A",
"description": "lorem ipsum..",
"type": "release",
"enabled": false,
"strategies": [
{
@ -89,6 +91,7 @@ Used to fetch details about a specific featureToggle. This is mostly provded to
{
"name": "Feature.A",
"description": "lorem ipsum..",
"type": "release",
"enabled": false,
"strategies": [
{
@ -99,7 +102,12 @@ Used to fetch details about a specific featureToggle. This is mostly provded to
}
```
Used by the admin-dashboard to create a new feature toggles. The name **must be unique**, otherwise you will get a _403-response_.
Used by the admin-dashboard to create a new feature toggles.
**Notes:**
- _name_ **must be globally unique**, otherwise you will get a _403-response_.
- _type_ is optional. If not defined it defaults to `release`
Returns 200-respose if the feature toggle was created successfully.
@ -113,6 +121,7 @@ Returns 200-respose if the feature toggle was created successfully.
{
"name": "Feature.A",
"description": "lorem ipsum..",
"type": "release",
"enabled": false,
"strategies": [
{
@ -150,6 +159,7 @@ None
{
"name": "Feature.A",
"description": "lorem ipsum..",
"type": "release",
"enabled": true,
"strategies": [
{
@ -177,6 +187,7 @@ None
{
"name": "Feature.A",
"description": "lorem ipsum..",
"type": "release",
"enabled": false,
"strategies": [
{
@ -205,6 +216,7 @@ Used to fetch list of archived feature toggles
{
"name": "Feature.A",
"description": "lorem ipsum",
"type": "release",
"enabled": false,
"strategies": [
{

View File

@ -0,0 +1,50 @@
---
id: events
title: /api/admin/feature-types
---
# Feature Types API
`GET: http://unleash.host.com/api/admin/feature-types`
Used to fetch all feature types defined in the unleash system.
**Response**
```json
{
"version": 1,
"types": [
{
"id": "release",
"name": "Release",
"description": "Used to enable trunk-based development for teams practicing Continuous Delivery.",
"lifetimeDays": 40
},
{
"id": "experiment",
"name": "Experiment",
"description": "Used to perform multivariate or A/B testing.",
"lifetimeDays": 40
},
{
"id": "ops",
"name": "Operational",
"description": "Used to control operational aspects of the system behavior.",
"lifetimeDays": 7
},
{
"id": "killswitch",
"name": "Kill switch",
"description": "Used to to gracefully degrade system functionality.",
"lifetimeDays": null
},
{
"id": "permission",
"name": "Permission",
"description": "Used to change the features or product experience that certain users receive.",
"lifetimeDays": null
}
]
}
```

View File

@ -27,6 +27,7 @@ This endpoint should never return anything besides a valid _20X or 304-response_
{
"name": "Feature.A",
"description": "lorem ipsum",
"type": "release",
"enabled": false,
"strategies": [
{
@ -39,6 +40,7 @@ This endpoint should never return anything besides a valid _20X or 304-response_
},
{
"name": "Feature.B",
"type": "killswitch",
"description": "lorem ipsum",
"enabled": true,
"strategies": [
@ -76,6 +78,7 @@ Used to fetch details about a specific feature toggle. This is mainly provided t
{
"name": "Feature.A",
"description": "lorem ipsum..",
"type": "release",
"enabled": false,
"strategies": [
{

View File

@ -9,21 +9,21 @@ This document describes our current database schema used in PostgreSQL. We use d
Used by db-migrate module to keep track of migrations.
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
| ------ | --------- | ---- | -------- | -------------------------------------- |
| id | serial | 10 | 0 | nextval('migrations_id_seq'::regclass) |
| name | varchar | 255 | 0 | (null) |
| run_on | timestamp | 29 | 0 | (null) |
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
| --- | --- | --- | --- | --- |
| id | serial | 10 | 0 | nextval('migrations_id_seq'::regclass) |
| name | varchar | 255 | 0 | (null) |
| run_on | timestamp | 29 | 0 | (null) |
## Table: _events_
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
| ---------- | --------- | ---------- | -------- | ---------------------------------- |
| id | serial | 10 | 0 | nextval('events_id_seq'::regclass) |
| created_at | timestamp | 29 | 1 | now() |
| type | varchar | 255 | 0 | (null) |
| created_by | varchar | 255 | 0 | (null) |
| data | json | 2147483647 | 1 | (null) |
| NAME | TYPE | SIZE | NULLABLE | COLUMN_DEF |
| --- | --- | --- | --- | --- |
| id | serial | 10 | 0 | nextval('events_id_seq'::regclass) |
| created_at | timestamp | 29 | 1 | now() |
| type | varchar | 255 | 0 | (null) |
| created_by | varchar | 255 | 0 | (null) |
| data | json | 2147483647 | 1 | (null) |
## Table: _strategies_
@ -36,14 +36,15 @@ Used by db-migrate module to keep track of migrations.
## Table: _features_
| **NAME** | **TYPE** | **SIZE** | **NULLABLE** | **COLUMN_DEF** | **COMMENT** |
| ----------- | --------- | ---------- | ------------ | -------------- | ----------- |
| created_at | timestamp | 29 | 1 | now() | |
| name | varchar | 255 | 0 | (null) | |
| enabled | int4 | 10 | 1 | 0 | |
| description | text | 2147483647 | 1 | (null) | |
| archived | int4 | 10 | 1 | 0 | |
| strategies | json | 2147483647 | 1 | (null) | |
| **NAME** | **TYPE** | **SIZE** | **NULLABLE** | **COLUMN_DEF** | **COMMENT** |
| --- | --- | --- | --- | --- | --- |
| created_at | timestamp | 29 | 1 | now() | |
| name | varchar | 255 | 0 | (null) | |
| enabled | int4 | 10 | 1 | 0 | |
| description | text | 2147483647 | 1 | (null) | |
| archived | int4 | 10 | 1 | 0 | |
| strategies | json | 2147483647 | 1 | (null) | |
| type | varchar | 2147483647 | 1 | release | |
## Table: _client_strategies_
@ -65,8 +66,17 @@ Used by db-migrate module to keep track of migrations.
## Table: _client_metrics_
| COLUMN_NAME | TYPE_NAME | COLUMN_SIZE | NULLABLE | COLUMN_DEF |
| ----------- | --------- | ----------- | -------- | ------------------------------------------ |
| id | serial | 10 | 0 | nextval('client_metrics_id_seq'::regclass) |
| created_at | timestamp | 29 | 1 | now() |
| metrics | json | 2147483647 | 1 | (null) |
| COLUMN_NAME | TYPE_NAME | COLUMN_SIZE | NULLABLE | COLUMN_DEF |
| --- | --- | --- | --- | --- |
| id | serial | 10 | 0 | nextval('client_metrics_id_seq'::regclass) |
| created_at | timestamp | 29 | 1 | now() |
| metrics | json | 2147483647 | 1 | (null) |
## Table: _feature_types_
| COLUMN_NAME | TYPE_NAME | COLUMN_SIZE | NULLABLE | COLUMN_DEF |
| ------------- | --------- | ----------- | -------- | ---------- |
| id | varchar | 255 | 0 | (null) |
| name | varchar | | 0 | (null) |
| description | varchar | | 1 | (null) |
| lifetime_days | integer | | 1 | (null) |

View File

@ -13,6 +13,7 @@ const NotFoundError = require('../error/notfound-error');
const FEATURE_COLUMNS = [
'name',
'description',
'type',
'enabled',
'strategies',
'variants',
@ -97,6 +98,7 @@ class FeatureToggleStore {
return {
name: row.name,
description: row.description,
type: row.type,
enabled: row.enabled > 0,
strategies: row.strategies,
variants: row.variants,
@ -108,6 +110,7 @@ class FeatureToggleStore {
return {
name: data.name,
description: data.description,
type: data.type,
enabled: data.enabled ? 1 : 0,
archived: data.archived ? 1 : 0,
strategies: JSON.stringify(data.strategies),

View File

@ -0,0 +1,29 @@
'use strict';
const COLUMNS = ['id', 'name', 'description', 'lifetime_days'];
const TABLE = 'feature_types';
class FeatureToggleStore {
constructor(db, getLogger) {
this.db = db;
this.getLogger = getLogger('feature-type-store.js');
}
getAll() {
return this.db
.select(COLUMNS)
.from(TABLE)
.map(this.rowToFeatureType);
}
rowToFeatureType(row) {
return {
id: row.id,
name: row.name,
description: row.description,
lifetimeDays: row.lifetime_days,
};
}
}
module.exports = FeatureToggleStore;

View File

@ -3,6 +3,7 @@
const { createDb } = require('./db-pool');
const EventStore = require('./event-store');
const FeatureToggleStore = require('./feature-toggle-store');
const FeatureTypeStore = require('./feature-type-store');
const StrategyStore = require('./strategy-store');
const ClientInstanceStore = require('./client-instance-store');
const ClientMetricsDb = require('./client-metrics-db');
@ -22,6 +23,7 @@ module.exports.createStores = (config, eventBus) => {
db,
eventStore,
featureToggleStore: new FeatureToggleStore(db, eventStore, getLogger),
featureTypeStore: new FeatureTypeStore(db, getLogger),
strategyStore: new StrategyStore(db, eventStore, getLogger),
clientApplicationsStore: new ClientApplicationsStore(
db,

View File

@ -54,6 +54,7 @@ const featureShema = joi
.keys({
name: nameType,
enabled: joi.boolean().default(false),
type: joi.string().default('release'),
description: joi
.string()
.allow('')

View File

@ -17,6 +17,7 @@ test('should require URL firendly name', t => {
test('should be valid toggle name', t => {
const toggle = {
name: 'app.name',
type: 'release',
enabled: false,
strategies: [{ name: 'default' }],
};
@ -28,6 +29,7 @@ test('should be valid toggle name', t => {
test('should strip extra variant fields', t => {
const toggle = {
name: 'app.name',
type: 'release',
enabled: false,
strategies: [{ name: 'default' }],
variants: [
@ -47,6 +49,7 @@ test('should strip extra variant fields', t => {
test('should allow weightType=fix', t => {
const toggle = {
name: 'app.name',
type: 'release',
enabled: false,
strategies: [{ name: 'default' }],
variants: [
@ -65,6 +68,7 @@ test('should allow weightType=fix', t => {
test('should disallow weightType=unknown', t => {
const toggle = {
name: 'app.name',
type: 'release',
enabled: false,
strategies: [{ name: 'default' }],
variants: [
@ -86,6 +90,7 @@ test('should disallow weightType=unknown', t => {
test('should be possible to define variant overrides', t => {
const toggle = {
name: 'app.name',
type: 'release',
enabled: false,
strategies: [{ name: 'default' }],
variants: [
@ -112,6 +117,7 @@ test('variant overrides must have corect shape', async t => {
t.plan(1);
const toggle = {
name: 'app.name',
type: 'release',
enabled: false,
strategies: [{ name: 'default' }],
variants: [
@ -139,6 +145,7 @@ test('variant overrides must have corect shape', async t => {
test('should keep constraints', t => {
const toggle = {
name: 'app.constraints',
type: 'release',
enabled: false,
strategies: [
{

View File

@ -0,0 +1,22 @@
'use strict';
const Controller = require('../controller');
const version = 1;
class FeatureTypeController extends Controller {
constructor(config) {
super(config);
this.featureTypeStore = config.stores.featureTypeStore;
this.logger = config.getLogger('/admin-api/feature-type.js');
this.get('/', this.getAllFeatureTypes);
}
async getAllFeatureTypes(req, res) {
const types = await this.featureTypeStore.getAll();
res.json({ version, types });
}
}
module.exports = FeatureTypeController;

View File

@ -2,6 +2,7 @@
const Controller = require('../controller');
const FeatureController = require('./feature.js');
const FeatureTypeController = require('./feature-type.js');
const ArchiveController = require('./archive.js');
const EventController = require('./event.js');
const StrategyController = require('./strategy');
@ -18,6 +19,10 @@ class AdminApi extends Controller {
this.app.get('/', this.index);
this.app.use('/features', new FeatureController(config).router);
this.app.use(
'/feature-types',
new FeatureTypeController(config).router,
);
this.app.use('/archive', new ArchiveController(config).router);
this.app.use('/strategies', new StrategyController(config).router);
this.app.use('/events', new EventController(config).router);

View File

@ -0,0 +1,38 @@
/* eslint camelcase: "off" */
'use strict';
const async = require('async');
exports.up = function(db, cb) {
async.series(
[
db.createTable.bind(db, 'feature_types', {
id: {
type: 'string',
length: 255,
primaryKey: true,
notNull: true,
},
name: { type: 'string', notNull: true },
description: { type: 'string' },
lifetime_days: { type: 'int' },
}),
db.runSql.bind(
db,
`
INSERT INTO feature_types(id, name, description, lifetime_days) VALUES('release', 'Release', 'Used to enable trunk-based development for teams practicing Continuous Delivery.', 40);
INSERT INTO feature_types(id, name, description, lifetime_days) VALUES('experiment', 'Experiment', 'Used to perform multivariate or A/B testing.', 40);
INSERT INTO feature_types(id, name, description, lifetime_days) VALUES('operational', 'Operational', 'Used to control operational aspects of the system behavior.', 7);
INSERT INTO feature_types(id, name, description, lifetime_days) VALUES('kill-switch', 'Kill switch', 'Used to to gracefully degrade system functionality.', null);
INSERT INTO feature_types(id, name, description, lifetime_days) VALUES('permission', 'Permission', 'Used to change the features or product experience that certain users receive.', null);
`,
),
],
cb,
);
};
exports.down = function(db, cb) {
return db.dropTable('feature_types', cb);
};

View File

@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, cb) {
return db.addColumn(
'features',
'type',
{
type: 'string',
defaultValue: 'release',
},
cb,
);
};
exports.down = function(db, cb) {
return db.removeColumn('features', 'type', cb);
};

View File

@ -87,7 +87,7 @@
"prom-client": "^12.0.0",
"response-time": "^2.3.2",
"serve-favicon": "^2.5.0",
"unleash-frontend": "3.4.0",
"unleash-frontend": "3.4.1-0",
"yargs": "^15.1.0"
},
"devDependencies": {

View File

@ -250,3 +250,30 @@ test.serial('creates new feature toggle with variant overrides', async t => {
.set('Content-Type', 'application/json')
.expect(201);
});
test.serial('creates new feature toggle without type', async t => {
t.plan(1);
const request = await setupApp(stores);
await request.post('/api/admin/features').send({
name: 'com.test.noType',
enabled: false,
strategies: [{ name: 'default' }],
});
await request.get('/api/admin/features/com.test.noType').expect(res => {
t.is(res.body.type, 'release');
});
});
test.serial('creates new feature toggle with type', async t => {
t.plan(1);
const request = await setupApp(stores);
await request.post('/api/admin/features').send({
name: 'com.test.withType',
type: 'killswitch',
enabled: false,
strategies: [{ name: 'default' }],
});
await request.get('/api/admin/features/com.test.withType').expect(res => {
t.is(res.body.type, 'killswitch');
});
});

View File

@ -9,7 +9,7 @@ module.exports = () => {
if (toggle) {
return Promise.resolve(toggle);
}
return Promise.reject();
return Promise.reject(new Error('could not find toggle'));
},
hasFeature: name => {
const toggle = _features.find(f => f.name === name);

View File

@ -5554,10 +5554,10 @@ universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
unleash-frontend@3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-3.4.0.tgz#6df17f56904dab59e7c99765d5443e7716aa0734"
integrity sha512-ZHzaPSoBKZGyp+Bneo2vBhTUbc34aOBh6hxyjZm3v/ol7PpTt6D3opoIVe18UElLOAZft2Kjq2Gtv/1gvJx6pg==
unleash-frontend@3.4.1-0:
version "3.4.1-0"
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-3.4.1-0.tgz#480731a753059ad4b38c98b9a5f014eaa44d912b"
integrity sha512-skx+SlOFPHcrrQlY5UhNV6stxblUNaB0mGbZfbh7CdIuWMs1Y+KxVoorTqDaHV0zhyxKQvngzZOd696VnMxA4w==
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"