mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-15 01:16:22 +02:00
Add a version service (#729)
- Checks versions against https://version.unleash.run - Generates a unique instance id (uuid)
This commit is contained in:
parent
4902161b39
commit
b83387a84a
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## 3.x.x
|
## 3.x.x
|
||||||
|
|
||||||
|
- feat: check latest version
|
||||||
|
- feat: expose current and latest version to ui-config
|
||||||
- feat: Use express-session backed by postgres
|
- feat: Use express-session backed by postgres
|
||||||
|
|
||||||
## 3.12.0
|
## 3.12.0
|
||||||
|
@ -100,3 +100,11 @@ curl --location --request PUT 'http://localhost:4242/api/admin/features/Feature.
|
|||||||
]\
|
]\
|
||||||
}'\
|
}'\
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Version check
|
||||||
|
|
||||||
|
- Unleash checks that it uses the latest version by making a call to https://version.unleash.run.
|
||||||
|
- This is a cloud function storing instance id to our database for statistics.
|
||||||
|
- This request includes a unique instance id for your server.
|
||||||
|
- If you do not wish to check for upgrades define the environment variable `CHECK_VERSION` to anything else other than `true` before starting, and Unleash won't make any calls
|
||||||
|
- `export CHECK_VERSION=false`
|
||||||
|
@ -69,6 +69,12 @@ function defaultOptions() {
|
|||||||
enableLegacyRoutes: false,
|
enableLegacyRoutes: false,
|
||||||
extendedPermissions: false,
|
extendedPermissions: false,
|
||||||
publicFolder,
|
publicFolder,
|
||||||
|
versionCheck: {
|
||||||
|
url:
|
||||||
|
process.env.UNLEASH_VERSION_URL ||
|
||||||
|
'https://version.unleash.run',
|
||||||
|
enable: process.env.CHECK_VERSION || 'true',
|
||||||
|
},
|
||||||
enableRequestLogger: false,
|
enableRequestLogger: false,
|
||||||
adminAuthentication: process.env.ADMIN_AUTHENTICATION || 'unsecure',
|
adminAuthentication: process.env.ADMIN_AUTHENTICATION || 'unsecure',
|
||||||
ui: {},
|
ui: {},
|
||||||
|
@ -3,16 +3,24 @@
|
|||||||
const Controller = require('../controller');
|
const Controller = require('../controller');
|
||||||
|
|
||||||
class ConfigController extends Controller {
|
class ConfigController extends Controller {
|
||||||
constructor(config) {
|
constructor(config, { versionService }) {
|
||||||
super(config);
|
super(config);
|
||||||
this.uiConfig = { ...config.ui, version: config.version };
|
this.versionService = versionService;
|
||||||
|
this.uiConfig = {
|
||||||
|
...config.ui,
|
||||||
|
version: config.version,
|
||||||
|
};
|
||||||
this.get('/', this.getUIConfig);
|
this.get('/', this.getUIConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUIConfig(req, res) {
|
async getUIConfig(req, res) {
|
||||||
const config = this.uiConfig;
|
const config = this.uiConfig;
|
||||||
res.json(config);
|
if (this.versionService) {
|
||||||
|
const versionInfo = this.versionService.getVersionInfo();
|
||||||
|
res.json({ ...config, versionInfo });
|
||||||
|
} else {
|
||||||
|
res.json(config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ const TagService = require('./tag-service');
|
|||||||
const StrategyService = require('./strategy-service');
|
const StrategyService = require('./strategy-service');
|
||||||
const AddonService = require('./addon-service');
|
const AddonService = require('./addon-service');
|
||||||
const ContextService = require('./context-service');
|
const ContextService = require('./context-service');
|
||||||
|
const VersionService = require('./version-service');
|
||||||
|
|
||||||
module.exports.createServices = (stores, config) => {
|
module.exports.createServices = (stores, config) => {
|
||||||
const featureToggleService = new FeatureToggleService(stores, config);
|
const featureToggleService = new FeatureToggleService(stores, config);
|
||||||
@ -18,6 +19,7 @@ module.exports.createServices = (stores, config) => {
|
|||||||
const clientMetricsService = new ClientMetricsService(stores, config);
|
const clientMetricsService = new ClientMetricsService(stores, config);
|
||||||
const addonService = new AddonService(stores, config, tagTypeService);
|
const addonService = new AddonService(stores, config, tagTypeService);
|
||||||
const contextService = new ContextService(stores, config);
|
const contextService = new ContextService(stores, config);
|
||||||
|
const versionService = new VersionService(stores, config);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addonService,
|
addonService,
|
||||||
@ -29,5 +31,6 @@ module.exports.createServices = (stores, config) => {
|
|||||||
tagService,
|
tagService,
|
||||||
clientMetricsService,
|
clientMetricsService,
|
||||||
contextService,
|
contextService,
|
||||||
|
versionService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
62
src/lib/services/version-service.js
Normal file
62
src/lib/services/version-service.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
const TWO_DAYS = 48 * 60 * 60 * 1000;
|
||||||
|
class VersionService {
|
||||||
|
constructor(
|
||||||
|
{ settingStore },
|
||||||
|
{ getLogger, versionCheck, version, enterpriseVersion },
|
||||||
|
) {
|
||||||
|
this.logger = getLogger('lib/services/version-service.js');
|
||||||
|
this.settingStore = settingStore;
|
||||||
|
this.current = {
|
||||||
|
oss: version,
|
||||||
|
enterprise: enterpriseVersion,
|
||||||
|
};
|
||||||
|
if (versionCheck) {
|
||||||
|
if (versionCheck.url) {
|
||||||
|
this.versionCheckUrl = versionCheck.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versionCheck.enable === 'true') {
|
||||||
|
this.enabled = true;
|
||||||
|
this.checkLatestVersion();
|
||||||
|
setInterval(this.checkLatestVersion, TWO_DAYS);
|
||||||
|
} else {
|
||||||
|
this.enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkLatestVersion() {
|
||||||
|
if (this.enabled) {
|
||||||
|
const { id } = await this.settingStore.get('instanceInfo');
|
||||||
|
try {
|
||||||
|
const data = await fetch(this.versionCheckUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
versions: this.current,
|
||||||
|
id,
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}).then(res => res.json());
|
||||||
|
this.latest = {
|
||||||
|
oss: data.versions.oss,
|
||||||
|
enterprise: data.versions.enterprise,
|
||||||
|
};
|
||||||
|
this.isLatest = data.latest;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info('Could not check newest version', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getVersionInfo() {
|
||||||
|
return {
|
||||||
|
current: this.current,
|
||||||
|
latest: this.latest || {},
|
||||||
|
isLatest: this.isLatest || false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = VersionService;
|
118
src/lib/services/versions-service.test.js
Normal file
118
src/lib/services/versions-service.test.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
const test = require('ava');
|
||||||
|
const proxyquire = require('proxyquire').noCallThru();
|
||||||
|
const fetchMock = require('fetch-mock').sandbox();
|
||||||
|
const stores = require('../../test/fixtures/store');
|
||||||
|
const getLogger = require('../../test/fixtures/no-logger');
|
||||||
|
const version = require('../util/version');
|
||||||
|
|
||||||
|
const VersionService = proxyquire('./version-service', {
|
||||||
|
'node-fetch': fetchMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('yields current versions', async t => {
|
||||||
|
const testurl = 'https://version.test';
|
||||||
|
const { settingStore } = stores.createStores();
|
||||||
|
await settingStore.insert({
|
||||||
|
name: 'instanceInfo',
|
||||||
|
content: { id: '1234abc' },
|
||||||
|
});
|
||||||
|
const latest = {
|
||||||
|
oss: '4.0.0',
|
||||||
|
enterprise: '4.0.0',
|
||||||
|
};
|
||||||
|
fetchMock.mock(
|
||||||
|
{ url: testurl, method: 'POST' },
|
||||||
|
{
|
||||||
|
latest: false,
|
||||||
|
versions: latest,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const service = new VersionService(
|
||||||
|
{ settingStore },
|
||||||
|
{ getLogger, versionCheck: { url: testurl, enable: 'true' }, version },
|
||||||
|
);
|
||||||
|
await service.checkLatestVersion();
|
||||||
|
fetchMock.done();
|
||||||
|
const versionInfo = service.getVersionInfo();
|
||||||
|
t.is(versionInfo.current.oss, version);
|
||||||
|
t.falsy(versionInfo.current.enterprise);
|
||||||
|
t.is(versionInfo.latest.oss, latest.oss);
|
||||||
|
t.is(versionInfo.latest.enterprise, latest.enterprise);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('supports setting enterprise version as well', async t => {
|
||||||
|
const testurl = `https://version.test${Math.random() * 1000}`;
|
||||||
|
const { settingStore } = stores.createStores();
|
||||||
|
const enterpriseVersion = '3.7.0';
|
||||||
|
await settingStore.insert({
|
||||||
|
name: 'instanceInfo',
|
||||||
|
content: { id: '1234abc' },
|
||||||
|
});
|
||||||
|
const latest = {
|
||||||
|
oss: '4.0.0',
|
||||||
|
enterprise: '4.0.0',
|
||||||
|
};
|
||||||
|
fetchMock.mock(
|
||||||
|
{ url: testurl, method: 'POST' },
|
||||||
|
{
|
||||||
|
latest: false,
|
||||||
|
versions: latest,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const service = new VersionService(
|
||||||
|
{ settingStore },
|
||||||
|
{
|
||||||
|
getLogger,
|
||||||
|
versionCheck: { url: testurl, enable: 'true' },
|
||||||
|
version,
|
||||||
|
enterpriseVersion,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await service.checkLatestVersion();
|
||||||
|
fetchMock.done();
|
||||||
|
const versionInfo = service.getVersionInfo();
|
||||||
|
t.is(versionInfo.current.oss, version);
|
||||||
|
t.is(versionInfo.current.enterprise, enterpriseVersion);
|
||||||
|
t.is(versionInfo.latest.oss, latest.oss);
|
||||||
|
t.is(versionInfo.latest.enterprise, latest.enterprise);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
'if version check is not enabled should not make any calls',
|
||||||
|
async t => {
|
||||||
|
const testurl = `https://version.test${Math.random() * 1000}`;
|
||||||
|
const { settingStore } = stores.createStores();
|
||||||
|
const enterpriseVersion = '3.7.0';
|
||||||
|
await settingStore.insert({
|
||||||
|
name: 'instanceInfo',
|
||||||
|
content: { id: '1234abc' },
|
||||||
|
});
|
||||||
|
const latest = {
|
||||||
|
oss: '4.0.0',
|
||||||
|
enterprise: '4.0.0',
|
||||||
|
};
|
||||||
|
fetchMock.mock(
|
||||||
|
{ url: testurl, method: 'POST' },
|
||||||
|
{
|
||||||
|
latest: false,
|
||||||
|
versions: latest,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const service = new VersionService(
|
||||||
|
{ settingStore },
|
||||||
|
{
|
||||||
|
getLogger,
|
||||||
|
versionCheck: { url: testurl, enable: false },
|
||||||
|
version,
|
||||||
|
enterpriseVersion,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await service.checkLatestVersion();
|
||||||
|
t.false(fetchMock.called(testurl));
|
||||||
|
const versionInfo = service.getVersionInfo();
|
||||||
|
t.is(versionInfo.current.oss, version);
|
||||||
|
t.is(versionInfo.current.enterprise, enterpriseVersion);
|
||||||
|
t.falsy(versionInfo.latest.oss, latest.oss);
|
||||||
|
t.falsy(versionInfo.latest.enterprise, latest.enterprise);
|
||||||
|
},
|
||||||
|
);
|
19
src/migrations/20210218090213-generate-server-identifier.js
Normal file
19
src/migrations/20210218090213-generate-server-identifier.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
INSERT INTO settings(name, content) VALUES ('instanceInfo', json_build_object('id', gen_random_uuid()));
|
||||||
|
`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
DROP FROM settings WHERE name = 'instanceInfo'
|
||||||
|
`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
20
src/test/fixtures/fake-setting-store.js
vendored
20
src/test/fixtures/fake-setting-store.js
vendored
@ -1,6 +1,18 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = () => ({
|
module.exports = () => {
|
||||||
insert: () => Promise.resolve(),
|
const _settings = [];
|
||||||
get: () => Promise.resolve(),
|
return {
|
||||||
});
|
insert: setting => {
|
||||||
|
_settings.push(setting);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
get: name => {
|
||||||
|
const setting = _settings.find(s => s.name === name);
|
||||||
|
if (setting) {
|
||||||
|
return Promise.resolve(setting.content);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('Could not find setting'));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user