Skip to content

Commit

Permalink
Add a version service (#729)
Browse files Browse the repository at this point in the history
- Checks versions against https://version.unleash.run
- Generates a unique instance id (uuid)
  • Loading branch information
Christopher Kolstad committed Feb 19, 2021
1 parent 4902161 commit b83387a
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 8 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 3.x.x

- feat: check latest version
- feat: expose current and latest version to ui-config
- feat: Use express-session backed by postgres

## 3.12.0
Expand Down
8 changes: 8 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
6 changes: 6 additions & 0 deletions src/lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ function defaultOptions() {
enableLegacyRoutes: false,
extendedPermissions: false,
publicFolder,
versionCheck: {
url:
process.env.UNLEASH_VERSION_URL ||
'https://version.unleash.run',
enable: process.env.CHECK_VERSION || 'true',
},
enableRequestLogger: false,
adminAuthentication: process.env.ADMIN_AUTHENTICATION || 'unsecure',
ui: {},
Expand Down
16 changes: 12 additions & 4 deletions src/lib/routes/admin-api/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@
const Controller = require('../controller');

class ConfigController extends Controller {
constructor(config) {
constructor(config, { versionService }) {
super(config);
this.uiConfig = { ...config.ui, version: config.version };

this.versionService = versionService;
this.uiConfig = {
...config.ui,
version: config.version,
};
this.get('/', this.getUIConfig);
}

async getUIConfig(req, res) {
const config = this.uiConfig;
res.json(config);
if (this.versionService) {
const versionInfo = this.versionService.getVersionInfo();
res.json({ ...config, versionInfo });
} else {
res.json(config);
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/lib/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const TagService = require('./tag-service');
const StrategyService = require('./strategy-service');
const AddonService = require('./addon-service');
const ContextService = require('./context-service');
const VersionService = require('./version-service');

module.exports.createServices = (stores, config) => {
const featureToggleService = new FeatureToggleService(stores, config);
Expand All @@ -18,6 +19,7 @@ module.exports.createServices = (stores, config) => {
const clientMetricsService = new ClientMetricsService(stores, config);
const addonService = new AddonService(stores, config, tagTypeService);
const contextService = new ContextService(stores, config);
const versionService = new VersionService(stores, config);

return {
addonService,
Expand All @@ -29,5 +31,6 @@ module.exports.createServices = (stores, config) => {
tagService,
clientMetricsService,
contextService,
versionService,
};
};
62 changes: 62 additions & 0 deletions src/lib/services/version-service.js
Original file line number Diff line number Diff line change
@@ -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 changes: 118 additions & 0 deletions src/lib/services/versions-service.test.js
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 0 deletions src/migrations/20210218090213-generate-server-identifier.js
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 4 deletions src/test/fixtures/fake-setting-store.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
'use strict';

module.exports = () => ({
insert: () => Promise.resolve(),
get: () => Promise.resolve(),
});
module.exports = () => {
const _settings = [];
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'));
},
};
};

0 comments on commit b83387a

Please sign in to comment.