mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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 | ||||
| 
 | ||||
| - feat: check latest version | ||||
| - feat: expose current and latest version to ui-config | ||||
| - feat: Use express-session backed by postgres | ||||
| 
 | ||||
| ## 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, | ||||
|         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: {}, | ||||
|  | ||||
| @ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
| @ -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, | ||||
| @ -29,5 +31,6 @@ module.exports.createServices = (stores, config) => { | ||||
|         tagService, | ||||
|         clientMetricsService, | ||||
|         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'; | ||||
| 
 | ||||
| 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')); | ||||
|         }, | ||||
|     }; | ||||
| }; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user