updater: move app update logic and attach the manual update reason

This commit is contained in:
Girish Ramakrishnan
2025-06-20 19:49:20 +02:00
parent 1ffad1ebaf
commit 84297ff473
2 changed files with 65 additions and 69 deletions

View File

@@ -79,9 +79,6 @@ exports = module.exports = {
checkManifest,
canAutoupdateApp,
autoupdateApps,
restoreApps,
configureApps,
schedulePendingTasks,
@@ -761,6 +758,38 @@ function postProcess(result) {
delete result.devicesJson;
}
// note: this value cannot be cached as it depends on enableAutomaticUpdate and runState
function canAutoupdateApp(app, updateInfo) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof updateInfo, 'object');
const manifest = updateInfo.manifest;
if (!app.enableAutomaticUpdate) return { code: false, reason: 'Automatic updates for the app is disabled' };
// for invalid subscriptions the appstore does not return a dockerImage
if (!manifest.dockerImage) return { code: false, reason: 'Invalid or Expired subscription '};
if (updateInfo.unstable) return { code: false, reason: 'Update is marked as unstable' }; // only manual update allowed for unstable updates
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(manifest.version))) {
return { code: false, reason: 'Major package version requires review of breaking changes' }; // major changes are blocking
}
if (app.runState === exports.RSTATE_STOPPED) return { code: false, reason: 'Stopped apps cannot run migration scripts' };
const newTcpPorts = manifest.tcpPorts || {};
const newUdpPorts = manifest.udpPorts || {};
const portBindings = app.portBindings; // this is never null
for (const portName in portBindings) {
if (!(portName in newTcpPorts) && !(portName in newUdpPorts)) return { code: false, reason: `${portName} port was in use but new update removes it` };
}
// it's fine if one or more (unused) port keys got removed
return { code: true, reason: '' };
}
// attaches computed properties
function attachProperties(app, domainObjectMap) {
assert.strictEqual(typeof app, 'object');
@@ -771,7 +800,14 @@ function attachProperties(app, domainObjectMap) {
app.secondaryDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
app.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
app.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
app.updateInfo = updateChecker.getAppUpdateInfoSync(app.id);
const updateInfo = updateChecker.getAppUpdateInfoSync(app.id);
if (updateInfo) {
const { code, reason } = canAutoupdateApp(app, updateInfo); // isAutoUpdatable is not cached since it depends on enableAutomaticUpdate and runState
updateInfo.isAutoUpdatable = code;
updateInfo.manualUpdateReason = reason;
}
app.updateInfo = updateInfo;
}
function isAdmin(user) {
@@ -2728,64 +2764,6 @@ async function getExec(app, execId) {
return await docker.getExec(execId);
}
function canAutoupdateApp(app, updateInfo) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof updateInfo, 'object');
const manifest = updateInfo.manifest;
if (!app.enableAutomaticUpdate) return false;
// for invalid subscriptions the appstore does not return a dockerImage
if (!manifest.dockerImage) return false;
if (updateInfo.unstable) return false; // only manual update allowed for unstable updates
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(manifest.version))) return false; // major changes are blocking
if (app.runState === exports.RSTATE_STOPPED) return false; // stopped apps won't run migration scripts and shouldn't be updated
const newTcpPorts = manifest.tcpPorts || { };
const newUdpPorts = manifest.udpPorts || { };
const portBindings = app.portBindings; // this is never null
for (const portName in portBindings) {
if (!(portName in newTcpPorts) && !(portName in newUdpPorts)) return false; // portName was in use but new update removes it
}
// it's fine if one or more (unused) keys got removed
return true;
}
async function autoupdateApps(updateInfo, auditSource) { // updateInfo is { appId -> { manifest } }
assert.strictEqual(typeof updateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
for (const appId of Object.keys(updateInfo)) {
const [getError, app] = await safe(get(appId));
if (getError) {
debug(`Cannot autoupdate app ${appId}: ${getError.message}`);
continue;
}
if (!canAutoupdateApp(app, updateInfo[appId])) {
debug(`app ${app.fqdn} requires manual update`);
await notifications.pin(notifications.TYPE_MANUAL_APP_UPDATE_NEEDED, `${app.manifest.title} at ${app.fqdn} requires manual update to version ${updateInfo[appId].manifest.version}`,
`Changelog:\n${updateInfo[appId].manifest.changelog}\n`, { context: app.id });
continue;
}
const data = {
manifest: updateInfo[appId].manifest,
force: false
};
debug(`app ${app.fqdn} will be automatically updated`);
const [updateError] = await safe(updateApp(app, data, auditSource));
if (updateError) debug(`Error autoupdating ${appId}. ${updateError.message}`);
}
}
async function backup(app, auditSource) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');