-
-
Notifications
You must be signed in to change notification settings - Fork 227
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(systemd): add doctor checks for systemd node version
- Loading branch information
Showing
5 changed files
with
319 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
const fs = require('fs-extra'); | ||
const get = require('lodash/get'); | ||
const path = require('path'); | ||
const ini = require('ini'); | ||
const chalk = require('chalk'); | ||
const semver = require('semver'); | ||
const execa = require('execa'); | ||
const {errors} = require('../../lib'); | ||
|
||
const {SystemError} = errors; | ||
|
||
const systemdEnabled = | ||
({instance}) => instance.config.get('process', 'local') === 'systemd'; | ||
|
||
const unitCheckTitle = 'Checking systemd unit file'; | ||
const nodeCheckTitle = 'Checking systemd node version'; | ||
|
||
async function checkUnitFile(ctx) { | ||
const unitFilePath = `/lib/systemd/system/ghost_${ctx.instance.name}.service`; | ||
ctx.systemd = {unitFilePath}; | ||
|
||
try { | ||
const contents = await fs.readFile(unitFilePath); | ||
ctx.systemd.unit = ini.parse(contents.toString('utf8').trim()); | ||
} catch (error) { | ||
throw new SystemError({ | ||
message: 'Unable to load or parse systemd unit file', | ||
err: error | ||
}); | ||
} | ||
} | ||
|
||
async function checkNodeVersion({instance, systemd, ui}, task) { | ||
const errBlock = { | ||
message: 'Unable to determine node version in use by systemd', | ||
help: `Ensure 'ExecStart' exists in ${chalk.cyan(systemd.unitFilePath)} and uses a valid Node version` | ||
}; | ||
|
||
const execStart = get(systemd, 'unit.Service.ExecStart', null); | ||
if (!execStart) { | ||
throw new SystemError(errBlock); | ||
} | ||
|
||
const [nodePath] = execStart.split(' '); | ||
let version; | ||
|
||
try { | ||
const stdout = await execa.stdout(nodePath, ['--version']); | ||
version = semver.valid(stdout.trim()); | ||
} catch (_) { | ||
throw new SystemError(errBlock); | ||
} | ||
|
||
if (!version) { | ||
throw new SystemError(errBlock); | ||
} | ||
|
||
task.title = `${nodeCheckTitle} - found v${version}`; | ||
|
||
if (!semver.eq(version, process.versions.node)) { | ||
ui.log( | ||
`Warning: Ghost is running with node v${version}.\n` + | ||
`Your current node version is v${process.versions.node}.`, | ||
'yellow' | ||
); | ||
} | ||
|
||
let nodeRange; | ||
|
||
try { | ||
const packagePath = path.join(instance.dir, 'current/package.json'); | ||
const ghostPkg = await fs.readJson(packagePath); | ||
nodeRange = get(ghostPkg, 'engines.node', null); | ||
} catch (_) { | ||
return; | ||
} | ||
|
||
if (!nodeRange) { | ||
return; | ||
} | ||
|
||
if (!semver.satisfies(version, nodeRange)) { | ||
throw new SystemError({ | ||
message: `Ghost v${instance.version} is not compatible with Node v${version}`, | ||
help: `Check the version of Node configured in ${chalk.cyan(systemd.unitFilePath)} and update it to a compatible version` | ||
}); | ||
} | ||
} | ||
|
||
module.exports = [{ | ||
title: unitCheckTitle, | ||
task: checkUnitFile, | ||
enabled: systemdEnabled, | ||
category: ['start'] | ||
}, { | ||
title: nodeCheckTitle, | ||
task: checkNodeVersion, | ||
enabled: systemdEnabled, | ||
category: ['start'] | ||
}]; | ||
|
||
// exports for unit testing | ||
module.exports.checkUnitFile = checkUnitFile; | ||
module.exports.checkNodeVersion = checkNodeVersion; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
const {expect, use} = require('chai'); | ||
const sinon = require('sinon'); | ||
|
||
const fs = require('fs-extra'); | ||
const execa = require('execa'); | ||
const {errors} = require('../../../lib'); | ||
|
||
const {checkUnitFile, checkNodeVersion} = require('../doctor'); | ||
|
||
use(require('chai-as-promised')); | ||
|
||
describe('Unit: Systemd > doctor checks', function () { | ||
afterEach(function () { | ||
sinon.restore(); | ||
}); | ||
|
||
describe('checkUnitFile', function () { | ||
it('errors when readFile errors', async function () { | ||
const readFile = sinon.stub(fs, 'readFile').rejects(new Error('test')); | ||
const ctx = { | ||
instance: {name: 'test'} | ||
}; | ||
|
||
const expectedPath = '/lib/systemd/system/ghost_test.service'; | ||
|
||
await expect(checkUnitFile(ctx)).to.be.rejectedWith(errors.SystemError); | ||
expect(readFile.calledOnceWithExactly(expectedPath)).to.be.true; | ||
expect(ctx.systemd).to.deep.equal({unitFilePath: expectedPath}); | ||
}); | ||
|
||
it('adds valid unit file to context', async function () { | ||
const readFile = sinon.stub(fs, 'readFile').resolves(` | ||
[Section1] | ||
Foo=Bar | ||
Baz = Bat | ||
[Section2] | ||
Test=Value | ||
`); | ||
|
||
const ctx = { | ||
instance: {name: 'test'} | ||
}; | ||
|
||
const expectedPath = '/lib/systemd/system/ghost_test.service'; | ||
const expectedCtx = { | ||
unitFilePath: expectedPath, | ||
unit: { | ||
Section1: { | ||
Foo: 'Bar', | ||
Baz: 'Bat' | ||
}, | ||
Section2: { | ||
Test: 'Value' | ||
} | ||
} | ||
}; | ||
|
||
await expect(checkUnitFile(ctx)).to.not.be.rejected; | ||
expect(readFile.calledOnceWithExactly(expectedPath)).to.be.true; | ||
expect(ctx.systemd).to.deep.equal(expectedCtx); | ||
}); | ||
}); | ||
|
||
describe('checkNodeVersion', function () { | ||
it('rejects if ExecStart line not found', async function () { | ||
const ctx = { | ||
systemd: { | ||
unitFilePath: '/tmp/unit-file', | ||
unit: {} | ||
} | ||
}; | ||
const task = {}; | ||
|
||
await expect(checkNodeVersion(ctx, task)).to.be.rejectedWith(errors.SystemError); | ||
}); | ||
|
||
it('rejects if node --version rejects', async function () { | ||
const stdout = sinon.stub(execa, 'stdout').rejects(new Error('test error')); | ||
|
||
const ctx = { | ||
systemd: { | ||
unitFilePath: '/tmp/unit-file', | ||
unit: { | ||
Service: { | ||
ExecStart: '/usr/bin/node /usr/bin/ghost' | ||
} | ||
} | ||
} | ||
}; | ||
const task = {}; | ||
|
||
await expect(checkNodeVersion(ctx, task)).to.be.rejectedWith(errors.SystemError); | ||
expect(stdout.calledOnceWithExactly('/usr/bin/node', ['--version'])).to.be.true; | ||
}); | ||
|
||
it('rejects if invalid semver', async function () { | ||
const stdout = sinon.stub(execa, 'stdout').resolves('not-valid-semver'); | ||
|
||
const ctx = { | ||
systemd: { | ||
unitFilePath: '/tmp/unit-file', | ||
unit: { | ||
Service: { | ||
ExecStart: '/usr/bin/node /usr/bin/ghost' | ||
} | ||
} | ||
} | ||
}; | ||
const task = {}; | ||
|
||
await expect(checkNodeVersion(ctx, task)).to.be.rejectedWith(errors.SystemError); | ||
expect(stdout.calledOnceWithExactly('/usr/bin/node', ['--version'])).to.be.true; | ||
}); | ||
|
||
it('returns if unable to parse ghost pkg json', async function () { | ||
const stdout = sinon.stub(execa, 'stdout').resolves('12.0.0'); | ||
const readJson = sinon.stub(fs, 'readJson').rejects(new Error('test')); | ||
const log = sinon.stub(); | ||
|
||
const ctx = { | ||
systemd: { | ||
unitFilePath: '/tmp/unit-file', | ||
unit: { | ||
Service: { | ||
ExecStart: '/usr/bin/node /usr/bin/ghost' | ||
} | ||
} | ||
}, | ||
ui: {log}, | ||
instance: {dir: '/var/www/ghost'} | ||
}; | ||
const task = {}; | ||
|
||
await expect(checkNodeVersion(ctx, task)).to.not.be.rejected; | ||
expect(stdout.calledOnceWithExactly('/usr/bin/node', ['--version'])).to.be.true; | ||
expect(task.title).to.equal('Checking systemd node version - found v12.0.0'); | ||
expect(readJson.calledOnceWithExactly('/var/www/ghost/current/package.json')).to.be.true; | ||
expect(log.calledOnce).to.be.true; | ||
}); | ||
|
||
it('returns if unable to find node range in ghost pkg json', async function () { | ||
const stdout = sinon.stub(execa, 'stdout').resolves(process.versions.node); | ||
const readJson = sinon.stub(fs, 'readJson').resolves({}); | ||
const log = sinon.stub(); | ||
|
||
const ctx = { | ||
systemd: { | ||
unitFilePath: '/tmp/unit-file', | ||
unit: { | ||
Service: { | ||
ExecStart: '/usr/bin/node /usr/bin/ghost' | ||
} | ||
} | ||
}, | ||
ui: {log}, | ||
instance: {dir: '/var/www/ghost'} | ||
}; | ||
const task = {}; | ||
|
||
await expect(checkNodeVersion(ctx, task)).to.not.be.rejected; | ||
expect(stdout.calledOnceWithExactly('/usr/bin/node', ['--version'])).to.be.true; | ||
expect(task.title).to.equal(`Checking systemd node version - found v${process.versions.node}`); | ||
expect(readJson.calledOnceWithExactly('/var/www/ghost/current/package.json')).to.be.true; | ||
expect(log.called).to.be.false; | ||
}); | ||
|
||
it('rejects if node version isn\'t compatible with Ghost' , async function () { | ||
const stdout = sinon.stub(execa, 'stdout').resolves(process.versions.node); | ||
const readJson = sinon.stub(fs, 'readJson').resolves({ | ||
engines: {node: '< 1.0.0'} | ||
}); | ||
const log = sinon.stub(); | ||
|
||
const ctx = { | ||
systemd: { | ||
unitFilePath: '/tmp/unit-file', | ||
unit: { | ||
Service: { | ||
ExecStart: '/usr/bin/node /usr/bin/ghost' | ||
} | ||
} | ||
}, | ||
ui: {log}, | ||
instance: {dir: '/var/www/ghost'} | ||
}; | ||
const task = {}; | ||
|
||
await expect(checkNodeVersion(ctx, task)).to.be.rejectedWith(errors.SystemError); | ||
expect(stdout.calledOnceWithExactly('/usr/bin/node', ['--version'])).to.be.true; | ||
expect(task.title).to.equal(`Checking systemd node version - found v${process.versions.node}`); | ||
expect(readJson.calledOnceWithExactly('/var/www/ghost/current/package.json')).to.be.true; | ||
expect(log.called).to.be.false; | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters