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

Merge pull request #689 from Unleash/feat-682-deprecate-strategies

Support deprecating and reactivating strategies
This commit is contained in:
Christopher Kolstad 2021-01-22 10:16:53 +01:00 committed by GitHub
commit 88dcbffe74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 282 additions and 6 deletions

View File

@ -5,6 +5,7 @@
- feat: add tags (#655)
- feat: add tag-types (#655)
- feat: Added servicelayer (#685)
- feat: Allow deprecation of strategies (#682)
- fix: upgrade knex to 0.21.15
- fix: Updated docs about event-types (#684)
- fix: Add application-created event (#595)

View File

@ -113,3 +113,23 @@ Used to create a new Strategy. Name is required and must be unique. It is also r
```
Used to update a Strategy definition. Name can't be changed. **PS! I can be dangerous to change a implemnted strategy as the implementation also might need to be changed**
### Deprecate strategy
`POST: https://unleash.host.com/api/admin/strategies/:name/deprecate`
Used to deprecate a strategy definition. This will set the deprecated flag to true. If the strategy is already deprecated, this will be a noop.
#### Errors
_404 NOT FOUND_ - if `:name` does not exist
### Reactivate strategy
`POST: https://unleash.host.com/api/admin/strategies/:name/reactivate`
Used to reactivate a deprecated strategy defintion. This will set the deprecated flag back to false. If the strategy is not deprecated this is a noop and will still return 200.
#### Errors
_404 NOT FOUND_ - if if `:name` does not exist

View File

@ -2,7 +2,13 @@
const NotFoundError = require('../error/notfound-error');
const STRATEGY_COLUMNS = ['name', 'description', 'parameters', 'built_in'];
const STRATEGY_COLUMNS = [
'name',
'description',
'parameters',
'built_in',
'deprecated',
];
const TABLE = 'strategies';
class StrategyStore {
@ -46,6 +52,7 @@ class StrategyStore {
editable: row.built_in !== 1,
description: row.description,
parameters: row.parameters,
deprecated: row.deprecated,
};
}
@ -57,6 +64,7 @@ class StrategyStore {
name: row.name,
description: row.description,
parameters: row.parameters,
deprecated: row.deprecated,
};
}
@ -85,6 +93,27 @@ class StrategyStore {
);
}
async deprecateStrategy({ name }) {
this.db(TABLE)
.where({ name })
.update({ deprecated: true })
.catch(err =>
this.logger.error('Could not deprecate strategy, error: ', err),
);
}
async reactivateStrategy({ name }) {
this.db(TABLE)
.where({ name })
.update({ deprecated: false })
.catch(err =>
this.logger.error(
'Could not reactivate strategy, error: ',
err,
),
);
}
async deleteStrategy({ name }) {
return this.db(TABLE)
.where({ name })

View File

@ -8,6 +8,8 @@ const {
STRATEGY_DELETED,
STRATEGY_UPDATED,
STRATEGY_IMPORT,
STRATEGY_DEPRECATED,
STRATEGY_REACTIVATED,
DROP_STRATEGIES,
FEATURE_CREATED,
FEATURE_UPDATED,
@ -35,6 +37,8 @@ const strategyTypes = [
STRATEGY_DELETED,
STRATEGY_UPDATED,
STRATEGY_IMPORT,
STRATEGY_DEPRECATED,
STRATEGY_REACTIVATED,
DROP_STRATEGIES,
];

View File

@ -12,6 +12,8 @@ module.exports = {
DROP_FEATURES: 'drop-features',
STRATEGY_CREATED: 'strategy-created',
STRATEGY_DELETED: 'strategy-deleted',
STRATEGY_DEPRECATED: 'strategy-deprecated',
STRATEGY_REACTIVATED: 'strategy-reactivated',
STRATEGY_UPDATED: 'strategy-updated',
STRATEGY_IMPORT: 'strategy-import',
DROP_STRATEGIES: 'drop-strategies',

View File

@ -23,6 +23,16 @@ class StrategyController extends Controller {
this.delete('/:name', this.removeStrategy, DELETE_STRATEGY);
this.post('/', this.createStrategy, CREATE_STRATEGY);
this.put('/:strategyName', this.updateStrategy, UPDATE_STRATEGY);
this.post(
'/:strategyName/deprecate',
this.deprecateStrategy,
UPDATE_STRATEGY,
);
this.post(
'/:strategyName/reactivate',
this.reactivateStrategy,
UPDATE_STRATEGY,
);
}
async getAllStratgies(req, res) {
@ -71,6 +81,38 @@ class StrategyController extends Controller {
handleErrors(res, this.logger, error);
}
}
async deprecateStrategy(req, res) {
const userName = extractUser(req);
const { strategyName } = req.params;
if (strategyName === 'default') {
res.status(403).end();
} else {
try {
await this.strategyService.deprecateStrategy(
strategyName,
userName,
);
res.status(200).end();
} catch (error) {
handleErrors(res, this.logger, error);
}
}
}
async reactivateStrategy(req, res) {
const userName = extractUser(req);
const { strategyName } = req.params;
try {
await this.strategyService.reactivateStrategy(
strategyName,
userName,
);
res.status(200).end();
} catch (error) {
handleErrors(res, this.logger, error);
}
}
}
module.exports = StrategyController;

View File

@ -196,3 +196,63 @@ test('editable=true will allow edit request', t => {
.send({ name, parameters: [] })
.expect(200);
});
test('deprecating a strategy works', async t => {
t.plan(1);
const name = 'editStrat';
const { request, base, strategyStore, perms } = getSetup();
perms.withPermissions(UPDATE_STRATEGY);
strategyStore.createStrategy({ name, parameters: [] });
await request
.post(`${base}/api/admin/strategies/${name}/deprecate`)
.send()
.expect(200);
return request
.get(`${base}/api/admin/strategies/${name}`)
.expect(200)
.expect(res => t.is(res.body.deprecated, true));
});
test('deprecating a non-existent strategy yields 404', t => {
t.plan(0);
const { request, base, perms } = getSetup();
perms.withPermissions(UPDATE_STRATEGY);
return request
.post(`${base}/api/admin/strategies/non-existent-strategy/deprecate`)
.expect(404);
});
test('reactivating a strategy works', async t => {
t.plan(1);
const name = 'editStrat';
const { request, base, strategyStore, perms } = getSetup();
perms.withPermissions(UPDATE_STRATEGY);
strategyStore.createStrategy({ name, parameters: [] });
await request
.post(`${base}/api/admin/strategies/${name}/reactivate`)
.send()
.expect(200);
return request
.get(`${base}/api/admin/strategies/${name}`)
.expect(200)
.expect(res => t.is(res.body.deprecated, false));
});
test('reactivating a non-existent strategy yields 404', t => {
t.plan(0);
const { request, base, perms } = getSetup();
perms.withPermissions(UPDATE_STRATEGY);
return request
.post(`${base}/api/admin/strategies/non-existent-strategy/reactivate`)
.expect(404);
});
test(`deprecating 'default' strategy will yield 403`, t => {
t.plan(0);
const { request, base, perms } = getSetup();
perms.withPermissions(UPDATE_STRATEGY);
return request
.post(`${base}/api/admin/strategies/default/deprecate`)
.expect(403);
});

View File

@ -6,10 +6,10 @@ const { filter } = require('./util');
const version = 1;
class FeatureController extends Controller {
constructor({ featureToggleService }) {
constructor({ featureToggleService }, getLogger) {
super();
this.toggleService = featureToggleService;
this.logger = getLogger('client-api/feature.js');
this.get('/', this.getAll);
this.get('/:featureName', this.getFeatureToggle);
}

View File

@ -19,7 +19,7 @@ class ClientMetricsController extends Controller {
await this.metrics.registerClientMetrics(data, clientIp);
return res.status(202).end();
} catch (e) {
this.logger.error('Failed to store metrics', e);
this.logger.warn('Failed to store metrics', e);
switch (e.name) {
case 'ValidationError':
return res.status(400).end();

View File

@ -17,7 +17,7 @@ class RegisterController extends Controller {
await this.metrics.registerClient(data, clientIp);
return res.status(202).end();
} catch (err) {
this.logger.error('failed to register client', err);
this.logger.warn('failed to register client', err);
switch (err.name) {
case 'ValidationError':
return res.status(400).end();

View File

@ -3,6 +3,8 @@ const NameExistsError = require('../error/name-exists-error');
const {
STRATEGY_CREATED,
STRATEGY_DELETED,
STRATEGY_DEPRECATED,
STRATEGY_REACTIVATED,
STRATEGY_UPDATED,
} = require('../event-type');
@ -34,8 +36,33 @@ class StrategyService {
});
}
async deprecateStrategy(strategyName, userName) {
await this.strategyStore.getStrategy(strategyName); // Check existence
await this.strategyStore.deprecateStrategy({ name: strategyName });
await this.eventStore.store({
type: STRATEGY_DEPRECATED,
createdBy: userName,
data: {
name: strategyName,
},
});
}
async reactivateStrategy(strategyName, userName) {
await this.strategyStore.getStrategy(strategyName); // Check existence
await this.strategyStore.reactivateStrategy({ name: strategyName });
await this.eventStore.store({
type: STRATEGY_REACTIVATED,
createdBy: userName,
data: {
name: strategyName,
},
});
}
async createStrategy(value, userName) {
const strategy = await strategySchema.validateAsync(value);
strategy.deprecated = false;
await this._validateStrategyName(strategy);
await this.strategyStore.createStrategy(strategy);
await this.eventStore.store({

View File

@ -0,0 +1,18 @@
'use strict';
exports.up = function(db, cb) {
db.runSql(
`
ALTER TABLE strategies ADD COLUMN deprecated boolean default false
`,
cb,
);
};
exports.down = function(db, cb) {
db.runSql(`ALTER TABLE strategies DROP COLUMN deprecated`, cb);
};
exports._meta = {
version: 1,
};

View File

@ -119,3 +119,56 @@ test.serial('cant update a unknown strategy', async t => {
.set('Content-Type', 'application/json')
.expect(404);
});
test.serial('deprecating a strategy works', async t => {
const request = await setupApp(stores);
const name = 'deprecate';
await request
.post('/api/admin/strategies')
.send({ name, description: 'Should deprecate this', parameters: [] })
.set('Content-Type', 'application/json')
.expect(201);
await request
.post(`/api/admin/strategies/${name}/deprecate`)
.send()
.expect(200);
await request
.get(`/api/admin/strategies/${name}`)
.expect('Content-Type', /json/)
.expect(200)
.expect(res => t.is(res.body.deprecated, true));
});
test.serial('can reactivate a deprecated strategy', async t => {
const request = await setupApp(stores);
const name = 'reactivate';
await request
.post('/api/admin/strategies')
.send({ name, description: 'Should deprecate this', parameters: [] })
.set('Content-Type', 'application/json')
.expect(201);
await request
.post(`/api/admin/strategies/${name}/deprecate`)
.send()
.expect(200);
await request
.get(`/api/admin/strategies/${name}`)
.expect('Content-Type', /json/)
.expect(200)
.expect(res => t.is(res.body.deprecated, true));
await request
.post(`/api/admin/strategies/${name}/reactivate`)
.send()
.expect(200);
await request
.get(`/api/admin/strategies/${name}`)
.expect('Content-Type', /json/)
.expect(200)
.expect(res => t.is(res.body.deprecated, false));
});
test.serial('cannot deprecate default strategy', async t => {
t.plan(0);
const request = await setupApp(stores);
await request.post('/api/admin/strategies/default/deprecate').expect(403);
});

View File

@ -3,7 +3,9 @@
const NotFoundError = require('../../lib/error/notfound-error');
module.exports = () => {
const _strategies = [{ name: 'default', editable: false, parameters: {} }];
const _strategies = [
{ name: 'default', editable: false, parameters: {}, deprecated: false },
];
return {
getStrategies: () => Promise.resolve(_strategies),
@ -31,5 +33,23 @@ module.exports = () => {
_strategies.indexOf(({ name }) => name === strat.name),
1,
),
deprecateStrategy: ({ name }) => {
const deprecatedStrat = _strategies.find(s => s.name === name);
deprecatedStrat.deprecated = true;
_strategies.splice(
_strategies.indexOf(s => name === s.name),
1,
);
_strategies.push(deprecatedStrat);
},
reactivateStrategy: ({ name }) => {
const reactivatedStrat = _strategies.find(s => s.name === name);
reactivatedStrat.deprecated = false;
_strategies.splice(
_strategies.indexOf(s => name === s.name),
1,
);
_strategies.push(reactivatedStrat);
},
};
};