serverless
Version:
Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more
160 lines (145 loc) • 5.83 kB
JavaScript
;
const _ = require('lodash');
const { format } = require('util');
const path = require('path');
const os = require('os');
const stream = require('stream');
const { promisify } = require('util');
const fse = require('fs-extra');
const spawn = require('child-process-ext/spawn');
const got = require('got');
const tar = require('tar');
const semver = require('semver');
const userConfig = require('@serverless/utils/config');
const currentVersion = require('../../package.json').version;
const standaloneUtils = require('./standalone');
const isNpmGlobalPackage = require('./npmPackage/isGlobal');
const isNpmPackageWritable = require('./npmPackage/isWritable');
const pipeline = promisify(stream.pipeline);
const npmInstallationDir = path.resolve(__dirname, '../../');
const serverlessTmpDir = path.resolve(os.tmpdir(), 'tmpdirs-serverless');
const CHECK_INTERVAL = 1000 * 60 * 30; // 30 minutes
const npmUpdate = async (serverless, { newVersion, tarballUrl, abortHandler }) => {
const npmPackageRoot = await isNpmPackageWritable(serverless);
if (!npmPackageRoot) {
serverless.cli.log(
`Auto update error: No write access to ${npmInstallationDir}`,
'Serverless',
{
color: 'orange',
}
);
return null;
}
const tempInstallationDir = path.resolve(serverlessTmpDir, `npm-update-${newVersion}`);
try {
await fse.remove(tempInstallationDir);
await fse.ensureDir(tempInstallationDir);
const downloadStream = got.stream(tarballUrl);
abortHandler.task = () => downloadStream.destroy();
await pipeline(downloadStream, tar.x({ cwd: tempInstallationDir, strip: 1 }));
const npmPromise = spawn('npm', ['install', '--production'], {
cwd: tempInstallationDir,
});
abortHandler.task = () => npmPromise.child.kill();
if ((await npmPromise).signal) return null;
await fse.remove(path.resolve(tempInstallationDir, 'package-lock.json'));
const tempOldInstallationDir = path.resolve(serverlessTmpDir, 'npm-old-installation');
await fse.remove(tempOldInstallationDir);
return async () => {
await fse.rename(npmInstallationDir, tempOldInstallationDir);
await fse.rename(tempInstallationDir, npmInstallationDir);
await fse.remove(tempOldInstallationDir);
};
} catch (error) {
if (!abortHandler.isAborted && process.env.SLS_DEBUG) {
serverless.cli.log(format('Auto update: Could not update npm installation: %O', error));
}
return null;
}
};
const standaloneUpdate = async (serverless, { newVersion, abortHandler }) => {
const executableUrl = await standaloneUtils.resolveUrl(`v${newVersion}`);
const tempStandalonePath = path.resolve(serverlessTmpDir, `executable-${newVersion}`);
try {
await fse.remove(tempStandalonePath);
const downloadStream = got.stream(executableUrl);
abortHandler.task = () => downloadStream.destroy();
await pipeline(downloadStream, fse.createWriteStream(tempStandalonePath));
await fse.chmod(tempStandalonePath, 0o755);
return () => fse.rename(tempStandalonePath, standaloneUtils.path);
} catch (error) {
if (!abortHandler.isAborted && process.env.SLS_DEBUG) {
serverless.cli.log(format('Auto update: Could not update npm installation: %O', error));
}
return null;
}
};
module.exports = async (serverless) => {
if (!serverless.onExitPromise) return; // Not intended for programmatic Serverless instances
if (serverless.isLocallyInstalled) return;
if (serverless.isStandaloneExecutable) {
if (process.platform === 'win32') return;
} else if (!(await isNpmGlobalPackage())) {
return;
}
const currentVersionData = semver.parse(currentVersion);
if (currentVersionData.prerelease.length) return;
const autoUpdateConfig = userConfig.get('autoUpdate');
if (!_.get(autoUpdateConfig, 'enabled')) return;
if (autoUpdateConfig.lastChecked + CHECK_INTERVAL > Date.now()) return;
const abortHandler = {};
serverless.onExitPromise.then(() => {
abortHandler.isAborted = true;
if (abortHandler.task) abortHandler.task();
});
const versionsRequest = got('https://registry.npmjs.org/serverless', {
headers: { accept: 'application/vnd.npm.install-v1+json' },
});
abortHandler.task = () => versionsRequest.cancel();
const versionRequestBody = await (async () => {
try {
return await versionsRequest;
} catch (error) {
if (!abortHandler.isAborted && process.env.SLS_DEBUG) {
serverless.cli.log(
format('Auto update: Could not resolve version info from npm: %O', error)
);
}
return null;
}
})();
if (!versionRequestBody) return;
let latestVersion = currentVersion;
let latestVersionMeta;
for (const [version, meta] of Object.entries(JSON.parse(versionRequestBody.body).versions)) {
if (meta.deprecated) continue;
const versionData = semver.parse(version);
if (versionData.prerelease.length) continue;
if (versionData.major !== currentVersionData.major) continue;
if (semver.gt(version, latestVersion)) {
latestVersion = version;
latestVersionMeta = meta;
}
}
if (latestVersion !== currentVersion) {
const updateTask = serverless.isStandaloneExecutable
? await standaloneUpdate(serverless, {
abortHandler,
newVersion: latestVersion,
})
: await npmUpdate(serverless, {
abortHandler,
newVersion: latestVersion,
tarballUrl: latestVersionMeta.dist.tarball,
});
abortHandler.task = null;
if (!updateTask) return;
serverless.onExitPromise.then(async () => {
await updateTask();
serverless.cli.log(`Sucessfully updated to v${latestVersion}`);
});
}
autoUpdateConfig.lastChecked = Date.now();
userConfig.set('autoUpdate', autoUpdateConfig);
};