diff --git a/CHANGES b/CHANGES index da361f1d7..68ae97903 100644 --- a/CHANGES +++ b/CHANGES @@ -3182,4 +3182,5 @@ * operator: fix viewing of backup progress and logs * notification: automatic app update failure notification * backup sites: identify conflicting site locations +* update: add policy to update apps and platform separately diff --git a/dashboard/public/translation/cs.json b/dashboard/public/translation/cs.json index 832eb2a30..1d4bab8d5 100644 --- a/dashboard/public/translation/cs.json +++ b/dashboard/public/translation/cs.json @@ -650,7 +650,6 @@ "updateAvailableAction": "Aktualizace k dispozici", "stopUpdateAction": "Zastavit aktualizaci", "disabled": "Zakázáno", - "schedule": "Naplánovat aktualizace", "onLatest": "poslední", "description": "Aktualizace platformy a aplikací se aplikují v nastavený čas podle časové zóny systému." }, @@ -658,10 +657,7 @@ "disableCheckbox": "Zakázat automatické aktualizace", "enableCheckbox": "Povolit automatické aktualizace", "selectOne": "Vyberte alespoň jeden den a čas", - "days": "Dny", - "hours": "Hodiny", - "description": "Nastavte dny a časy pro automatické aktualizace platformy a aplikací. Ujistěte se, že se tento plán nepřekrývá s plány zálohování.", - "title": "Konfigurovat čas automatických aktualizací" + "description": "Nastavte dny a časy pro automatické aktualizace platformy a aplikací. Ujistěte se, že se tento plán nepřekrývá s plány zálohování." }, "updateDialog": { "title": "Aktualizovat Cloudron", @@ -890,7 +886,8 @@ "appDown": "Aplikace je mimo provoz", "rebootRequired": "Vyžadován restart serveru", "cloudronUpdateFailed": "Aktualizace Cloudronu selhala", - "diskSpace": "Nedostatek místa na disku" + "diskSpace": "Nedostatek místa na disku", + "appAutoUpdateFailed": "Automatická aktualizace aplikace selhala" }, "settingsDialog": { "description": "Na váš primární e-mail bude odeslán e-mail souhrn těchto vybraných událostí." diff --git a/dashboard/public/translation/da.json b/dashboard/public/translation/da.json index e65e399c1..006d084a8 100644 --- a/dashboard/public/translation/da.json +++ b/dashboard/public/translation/da.json @@ -555,11 +555,8 @@ "updateScheduleDialog": { "selectOne": "Vælg mindst én dag og ét tidspunkt", "description": "Vælg de dage og timer, hvor Cloudron vil anvende automatiske platforms- og appopdateringer. Pas på, at denne tidsplan ikke overlapper med backup-tidsplanen.", - "title": "Konfigurer tidsplan for automatisk opdatering", "disableCheckbox": "Deaktivere automatiske opdateringer", - "enableCheckbox": "Aktivere automatiske opdateringer", - "days": "Dage", - "hours": "Timer" + "enableCheckbox": "Aktivere automatiske opdateringer" }, "updateDialog": { "unstableWarning": "Denne opdatering er en præudgave og betragtes ikke som stabil endnu. Opdatering sker på egen risiko.", diff --git a/dashboard/public/translation/de.json b/dashboard/public/translation/de.json index 4b268e94a..8ed677316 100644 --- a/dashboard/public/translation/de.json +++ b/dashboard/public/translation/de.json @@ -134,7 +134,6 @@ "updateAvailableAction": "Aktualisierung verfügbar", "description": "Plattform und App-Aktualisierungen werden automatisch, basierend auf dem Zeitplan in der Systemzeitzone ausgeführt.", "disabled": "Deaktiviert", - "schedule": "Aktualisierungszeitplan", "onLatest": "neueste" }, "appstoreAccount": { @@ -154,13 +153,10 @@ } }, "updateScheduleDialog": { - "hours": "Stunden", "disableCheckbox": "Automatische Aktualisierung deaktivieren", "enableCheckbox": "Automatische Aktualisierung aktivieren", "selectOne": "Mindestens einen Tag und eine Uhrzeit wählen", - "days": "Tage", - "description": "Tage und Stunden auswählen, an denen Cloudron das System und die Anwendungen aktualisieren soll. Der Zeitplan soll sich nicht mit dem Zeitplan für Datensicherungen überschneiden.", - "title": "Automatische Aktualisierung konfigurieren" + "description": "Tage und Stunden auswählen, an denen Cloudron das System und die Anwendungen aktualisieren soll. Der Zeitplan soll sich nicht mit dem Zeitplan für Datensicherungen überschneiden." }, "timezone": { "description": "Dient dazu, Datensicherungen und Updates zu planen. UI-Zeitstempel folgen immer der Zeitzone des Browsers.", diff --git a/dashboard/public/translation/en.json b/dashboard/public/translation/en.json index 365f1d745..3f4fdfeaf 100644 --- a/dashboard/public/translation/en.json +++ b/dashboard/public/translation/en.json @@ -707,17 +707,16 @@ "updateAvailableAction": "Update available", "stopUpdateAction": "Stop update", "disabled": "Disabled", - "schedule": "Update schedule", - "description": "Platform and app updates are applied on the configured schedule, using the System time zone.", - "onLatest": "latest" + "description": "Updates are applied on the configured schedule, using the System time zone.", + "onLatest": "latest", + "config": "Automatic updates", + "appsOnly": "Apps only", + "platformAndApps": "Platform & apps" }, "updateScheduleDialog": { - "title": "Configure Automatic Update Schedule", "disableCheckbox": "Disable automatic updates", "enableCheckbox": "Enable automatic updates", "selectOne": "Select at least one day and time", - "days": "Days", - "hours": "Hours", "description": "Set the days and times for automatic platform and app updates. Ensure this schedule doesn’t overlap with backup schedules." }, "updateDialog": { @@ -737,6 +736,14 @@ "registryConfig": { "provider": "Docker registry provider", "providerOther": "Other" + }, + "configureUpdates": { + "title": "Configure Automatic Updates", + "policy": "Policy", + "policyDescription": "Choose what gets updated automatically", + "days": "Days", + "hours": "Hours", + "schedule": "Schedule" } }, "support": { diff --git a/dashboard/public/translation/es.json b/dashboard/public/translation/es.json index 234c2d069..79f588b73 100644 --- a/dashboard/public/translation/es.json +++ b/dashboard/public/translation/es.json @@ -660,12 +660,9 @@ "title": "Ajustes", "updateScheduleDialog": { "description": "Establece los días y horarios para las actualizaciones automáticas de la plataforma y la aplicación. Asegúrate de que esta programación no coincida con la programación de las copias de seguridad.", - "hours": "Horas", - "days": "Días", "selectOne": "Seleccione al menos un día y una hora", "enableCheckbox": "Habilitar Actualizaciones Automáticas", - "disableCheckbox": "Desactivar las Actualizaciones Automáticas", - "title": "Configurar la programación de las Actualizaciones Automáticas" + "disableCheckbox": "Desactivar las Actualizaciones Automáticas" }, "updates": { "stopUpdateAction": "Parar Actualización", @@ -674,7 +671,6 @@ "title": "Actualizaciones", "description": "Las actualizaciones de la plataforma y de la aplicación se aplican según el cronograma establecido aquí, utilizando la Zona horaria del sistema.", "disabled": "Deshabilitado", - "schedule": "Programar", "onLatest": "el último" }, "language": { diff --git a/dashboard/public/translation/fr.json b/dashboard/public/translation/fr.json index 9268568c1..2686b37dc 100644 --- a/dashboard/public/translation/fr.json +++ b/dashboard/public/translation/fr.json @@ -517,12 +517,9 @@ }, "updateScheduleDialog": { "description": "Sélectionnez les jours et heures de lancement des mises à jour de la plateforme et des applications. Veillez à ne pas planifier les mises à jour au même moment que la sauvegarde.", - "hours": "Heures", - "days": "Jours", "selectOne": "Sélectionnez au moins un jour et une heure", "enableCheckbox": "Activer les mises à jour automatiques", - "disableCheckbox": "Désactiver les mises à jour automatiques", - "title": "Planification des mises à jour automatiques" + "disableCheckbox": "Désactiver les mises à jour automatiques" }, "updates": { "stopUpdateAction": "Interrompre la mise à jour", diff --git a/dashboard/public/translation/id.json b/dashboard/public/translation/id.json index f111227f6..11188fee5 100644 --- a/dashboard/public/translation/id.json +++ b/dashboard/public/translation/id.json @@ -727,17 +727,13 @@ "updateAvailableAction": "Pembaruan tersedia", "stopUpdateAction": "Hentikan pembaruan", "disabled": "Dinonaktifkan", - "schedule": "Jadwal pembaruan", "description": "Pembaruan platform dan aplikasi diterapkan sesuai jadwal yang telah dikonfigurasi, menggunakan Zona waktu sistem.", "onLatest": "terbaru" }, "updateScheduleDialog": { - "title": "Konfigurasi Jadwal Pembaruan Otomatis", "disableCheckbox": "Nonaktifkan pembaruan otomatis", "enableCheckbox": "Aktifkan pembaruan otomatis", "selectOne": "Pilih setidaknya satu hari dan satu waktu", - "days": "Hari", - "hours": "Jam", "description": "Atur hari dan waktu untuk pembaruan otomatis platform dan aplikasi. Pastikan jadwal ini tidak tumpang tindih dengan jadwal pencadangan." }, "updateDialog": { diff --git a/dashboard/public/translation/it.json b/dashboard/public/translation/it.json index e12a6b7a4..728cfb6cf 100644 --- a/dashboard/public/translation/it.json +++ b/dashboard/public/translation/it.json @@ -782,12 +782,9 @@ }, "updateScheduleDialog": { "description": "Seleziona i giorni e gli orari durante i quali Cloudron applicherà gli aggiornamenti automatici della piattaforma e dell'app. Fai attenzione a non sovrapporre questa pianificazione alla pianificazione dei backup.", - "hours": "Ore", - "days": "Giorni", "selectOne": "Seleziona almeno un giorno e un'ora", "enableCheckbox": "Abilita Aggiornamenti Automatici", - "disableCheckbox": "Disabilita Aggiornamenti Automatici", - "title": "Configura pianificazione aggiornamenti automatici" + "disableCheckbox": "Disabilita Aggiornamenti Automatici" }, "updates": { "stopUpdateAction": "Ferma Aggiornamento", diff --git a/dashboard/public/translation/nl.json b/dashboard/public/translation/nl.json index e655f5802..cdd133d06 100644 --- a/dashboard/public/translation/nl.json +++ b/dashboard/public/translation/nl.json @@ -1146,16 +1146,12 @@ "stopUpdateAction": "Stop update", "description": "Platform en app updates worden toegepast met de geconfigureerde planning met deze Systeem tijdzone.", "disabled": "Uitgeschakeld", - "schedule": "Update planning", "onLatest": "Laatste" }, "updateScheduleDialog": { "disableCheckbox": "Automatische updates uitschakelen", "enableCheckbox": "Automatische updates inschakelen", "selectOne": "Selecteer minstens één dag en tijd", - "days": "Dagen", - "hours": "Uren", - "title": "Automatische Update Planning configureren", "description": "Stel de dagen en uren in voor automatische updates van het platform en apps. Zorg ervoor dat dit schema niet overlapt met de back-upschema's." }, "updateDialog": { diff --git a/dashboard/public/translation/pt.json b/dashboard/public/translation/pt.json index e3203111d..4f3c5d3c0 100644 --- a/dashboard/public/translation/pt.json +++ b/dashboard/public/translation/pt.json @@ -619,7 +619,6 @@ }, "updates": { "checkForUpdatesAction": "Procurar por Atualizações", - "schedule": "Agendar", "updateAvailableAction": "Disponível Atualização", "stopUpdateAction": "Parar Atualização", "disabled": "Desativada" @@ -633,8 +632,6 @@ "blockingAppsInfo": "Por favor, aguarde que as operações em cima terminem." }, "updateScheduleDialog": { - "days": "Dias", - "hours": "Horas", "disableCheckbox": "Desativar Atualizações Automáticas", "enableCheckbox": "Ativar Atualizações Automáticas", "selectOne": "Selecione pelo menos um dia e hora" diff --git a/dashboard/public/translation/ru.json b/dashboard/public/translation/ru.json index 4ad8fad6d..e6a917bab 100644 --- a/dashboard/public/translation/ru.json +++ b/dashboard/public/translation/ru.json @@ -1050,17 +1050,13 @@ "updateAvailableAction": "Доступно обновление", "stopUpdateAction": "Остановить обновление", "description": "Обновления платформы и приложений запускаются с учётом установленного расписания и в соответствии с системным часовым поясом.", - "schedule": "Расписание обновлений", "disabled": "Выключено", "onLatest": "последний" }, "updateScheduleDialog": { - "title": "Настроить расписание автоматических обновлений", "disableCheckbox": "Выключить автоматические обновления", "enableCheckbox": "Включить автоматические обновления", "selectOne": "Выберите по крайней мере один день и время", - "days": "Дни", - "hours": "Часы", "description": "Установите дни и часы, в которые будет происходить автоматическое обновление платформы и приложений. Убедитесь, что установленное расписание не пересекается с расписанием резервного копирования." }, "updateDialog": { diff --git a/dashboard/public/translation/vi.json b/dashboard/public/translation/vi.json index 791abef43..759e99dc1 100644 --- a/dashboard/public/translation/vi.json +++ b/dashboard/public/translation/vi.json @@ -806,12 +806,9 @@ }, "updateScheduleDialog": { "description": "Chọn ngày và thời gian mà Cloudron sẽ tự động cập nhật phiên bản mới của hệ thống và app. Xin tránh chọn trùng lịch cập nhật này với lịch sao lưu.", - "hours": "Thời gian", "selectOne": "Xin chọn ít nhất một ngày và thời gian", - "days": "Ngày", "enableCheckbox": "Bật chế độ cập nhật tự động", - "disableCheckbox": "Tắt chế độ cập nhật tự động", - "title": "Cấu hình lịch cập nhật tự động" + "disableCheckbox": "Tắt chế độ cập nhật tự động" }, "updates": { "checkForUpdatesAction": "Kiểm tra cập nhật", @@ -819,7 +816,6 @@ "updateAvailableAction": "Có phiên bản cập nhật mới", "title": "Cập nhật", "disabled": "Đã tắt", - "schedule": "Lịch cập nhật", "description": "Cập nhật Hệ thống và Ứng dụng được thực hiện tự động dựa trên Lịch cập nhật trong Múi giờ hệ thống." }, "timezone": { diff --git a/dashboard/public/translation/zh_Hans.json b/dashboard/public/translation/zh_Hans.json index d246487e5..e2c000765 100644 --- a/dashboard/public/translation/zh_Hans.json +++ b/dashboard/public/translation/zh_Hans.json @@ -534,12 +534,9 @@ "stopUpdateAction": "停止更新" }, "updateScheduleDialog": { - "title": "配置自动更新时间表", "disableCheckbox": "停用自动更新", "enableCheckbox": "启用自动更新", "selectOne": "选择至少一个日期和时间", - "days": "星期", - "hours": "小时", "description": "选择检查平台和应用更新的日子和时间。请注意这个时间不要和 备份时间 冲突。" }, "updateDialog": { diff --git a/dashboard/src/components/SystemUpdate.vue b/dashboard/src/components/SystemUpdate.vue index fcee07889..69c6e806c 100644 --- a/dashboard/src/components/SystemUpdate.vue +++ b/dashboard/src/components/SystemUpdate.vue @@ -30,7 +30,8 @@ const taskLogsMenu = ref([]); const apps = ref([]); const version = ref(''); const ubuntuVersion = ref(''); -const currentPattern = ref(''); +const currentSchedule = ref(''); +const currentPolicy = ref(''); const updateBusy = ref(false); const updateError = ref({}); const stopError = ref({}); @@ -55,17 +56,16 @@ const inProgressApps = computed(() => { const configureDialog = useTemplateRef('configureDialog'); const configureBusy = ref(false); const configureError = ref(''); -const configureType = ref(''); -const configurePattern = ref(''); +const configurePolicy = ref(''); const configureDays = ref([]); const configureHours = ref([]); -async function refreshAutoupdatePattern() { - const [error, result] = await updaterModel.getAutoupdatePattern(); +async function refreshAutoupdateConfig() { + const [error, result] = await updaterModel.getAutoupdateConfig(); if (error) return console.error(error); - currentPattern.value = result.pattern; - configurePattern.value = result.pattern; + currentSchedule.value = result.schedule; + currentPolicy.value = result.policy; } async function refreshApps() { @@ -87,21 +87,22 @@ async function refreshPendingUpdateInfo() { } function onShowConfigure() { - if (currentPattern.value === 'never') { - configureType.value = 'never'; - } else { - configureType.value = 'pattern'; - const result = parseSchedule(currentPattern.value); - configureDays.value = result.days; // Array of cronDays.id - configureHours.value = result.hours; // Array of cronHours.id + configurePolicy.value = currentPolicy.value || 'never'; + + if (currentPolicy.value !== 'never') { + const result = parseSchedule(currentSchedule.value); + configureDays.value = result.days; + configureHours.value = result.hours; } configureDialog.value.open(); } async function onSubmitConfigure() { - let pattern = 'never'; - if (configureType.value === 'pattern') { + let schedule = currentSchedule.value || '00 00 1,3,5,23 * * *'; + const policy = configurePolicy.value; + + if (policy !== 'never') { let daysPattern; if (configureDays.value.length === 7) daysPattern = '*'; else daysPattern = configureDays.value.join(','); @@ -110,18 +111,18 @@ async function onSubmitConfigure() { if (configureHours.value.length === 24) hoursPattern = '*'; else hoursPattern = configureHours.value.join(','); - pattern ='00 00 ' + hoursPattern + ' * * ' + daysPattern; + schedule = '00 00 ' + hoursPattern + ' * * ' + daysPattern; } configureBusy.value = true; - const [error] = await updaterModel.setAutoupdatePattern(pattern); + const [error] = await updaterModel.setAutoupdateConfig(schedule, policy); if (error) { configureError.value = error.body ? error.body.message : 'Internal error'; configureBusy.value = false; return console.error(error); } - await refreshAutoupdatePattern(); + await refreshAutoupdateConfig(); configureBusy.value = false; configureDialog.value.close(); @@ -239,7 +240,7 @@ onMounted(async () => { ubuntuVersion.value = result.ubuntuVersion; await refreshPendingUpdateInfo(); - await refreshAutoupdatePattern(); + await refreshAutoupdateConfig(); await refreshTasks(); ready.value = true; @@ -288,25 +289,35 @@ onMounted(async () => { -
-

{{ configureError }}

- - + +
{{ $t('settings.configureUpdates.policyDescription') }}
+
+ + + +
+
-
-
{{ $t('settings.updateScheduleDialog.days') }}:
-
{{ $t('settings.updateScheduleDialog.hours') }}:
-
{{ $t('settings.updateScheduleDialog.selectOne') }}
+ +
+ +
+
{{ $t('settings.configureUpdates.days') }}:
+
{{ $t('settings.configureUpdates.hours') }}:
+
{{ $t('settings.updateScheduleDialog.selectOne') }}
+
@@ -321,9 +332,10 @@ onMounted(async () => {
- - {{ prettySchedule(currentPattern) }} - {{ $t('settings.updates.disabled') }} + + {{ $t('settings.updates.disabled') }} + {{ $t('settings.updates.appsOnly') }} - {{ prettySchedule(currentSchedule) }} + {{ $t('settings.updates.platformAndApps') }} - {{ prettySchedule(currentSchedule) }}
diff --git a/dashboard/src/models/UpdaterModel.js b/dashboard/src/models/UpdaterModel.js index ee00be6b4..928f13c52 100644 --- a/dashboard/src/models/UpdaterModel.js +++ b/dashboard/src/models/UpdaterModel.js @@ -17,10 +17,10 @@ function create() { if (error || result.status !== 200) return [error || result]; return [null, result.body.update]; }, - async getAutoupdatePattern() { + async getAutoupdateConfig() { let error, result; try { - result = await fetcher.get(`${API_ORIGIN}/api/v1/updater/autoupdate_pattern`, { access_token: accessToken }); + result = await fetcher.get(`${API_ORIGIN}/api/v1/updater/autoupdate_config`, { access_token: accessToken }); } catch (e) { error = e; } @@ -28,10 +28,10 @@ function create() { if (error || result.status !== 200) return [error || result]; return [null, result.body]; }, - async setAutoupdatePattern(pattern) { + async setAutoupdateConfig(schedule, policy) { let error, result; try { - result = await fetcher.post(`${API_ORIGIN}/api/v1/updater/autoupdate_pattern`, { pattern }, { access_token: accessToken }); + result = await fetcher.post(`${API_ORIGIN}/api/v1/updater/autoupdate_config`, { schedule, policy }, { access_token: accessToken }); } catch (e) { error = e; } diff --git a/migrations/20260315000000-settings-split-autoupdate.js b/migrations/20260315000000-settings-split-autoupdate.js new file mode 100644 index 000000000..d9e345fec --- /dev/null +++ b/migrations/20260315000000-settings-split-autoupdate.js @@ -0,0 +1,24 @@ +'use strict'; + +exports.up = async function(db) { + const results = await db.runSql('SELECT * FROM settings WHERE name=?', ['autoupdate_pattern']); + + let policy, schedule; + + if (results.length === 0 || results[0].value === 'never') { + policy = 'never'; + schedule = '00 00 1,3,5,23 * * *'; + } else { + policy = 'platform_and_apps'; + schedule = results[0].value; + } + + await db.runSql('START TRANSACTION;'); + await db.runSql('INSERT settings (name, value) VALUES(?, ?)', ['autoupdate_schedule', schedule]); + await db.runSql('INSERT settings (name, value) VALUES(?, ?)', ['autoupdate_policy', policy]); + await db.runSql('DELETE FROM settings WHERE name=?', ['autoupdate_pattern']); + await db.runSql('COMMIT'); +}; + +exports.down = async function() { +}; diff --git a/src/cron.js b/src/cron.js index 3bad706be..e56a418bd 100644 --- a/src/cron.js +++ b/src/cron.js @@ -111,20 +111,20 @@ async function handleBackupScheduleChanged(site) { gJobs.backups.set(site.id, job); } -async function handleAutoupdatePatternChanged(pattern) { - assert.strictEqual(typeof pattern, 'string'); +async function handleAutoupdateConfigChanged(config) { + assert.strictEqual(typeof config, 'object'); const tz = await cloudron.getTimeZone(); - log(`autoupdatePatternChanged: pattern - ${pattern} (${tz})`); + log(`handleAutoupdateConfigChanged: schedule - ${config.schedule}/${tz}, policy - ${config.policy}`); if (gJobs.autoUpdater) gJobs.autoUpdater.stop(); gJobs.autoUpdater = null; - if (pattern === constants.CRON_PATTERN_NEVER) return; + if (config.policy === updater.AUTOUPDATE_POLICY_NEVER) return; gJobs.autoUpdater = CronJob.from({ - cronTime: pattern, + cronTime: config.schedule, onTick: async () => await safe(updater.autoUpdate(AuditSource.CRON), { debug: log }), start: true, timeZone: tz @@ -268,7 +268,8 @@ async function startJobs() { for (const backupSite of await backupSites.list()) { await handleBackupScheduleChanged(backupSite); } - await handleAutoupdatePatternChanged(await updater.getAutoupdatePattern()); + const autoupdateConfig = await updater.getAutoupdateConfig(); + await handleAutoupdateConfigChanged(autoupdateConfig); await handleDynamicDnsChanged(await network.getDynamicDns()); await handleExternalLdapChanged(await externalLdap.getConfig()); } @@ -302,7 +303,7 @@ export default { handleBackupScheduleChanged, handleTimeZoneChanged, - handleAutoupdatePatternChanged, + handleAutoupdateConfigChanged, handleDynamicDnsChanged, handleExternalLdapChanged, diff --git a/src/routes/test/updater-test.js b/src/routes/test/updater-test.js index f257ff3cf..775635e0f 100644 --- a/src/routes/test/updater-test.js +++ b/src/routes/test/updater-test.js @@ -1,8 +1,8 @@ import { describe, it, before, after } from 'mocha'; import common from './common.js'; -import constants from '../../constants.js'; import assert from 'node:assert/strict'; import superagent from '@cloudron/superagent'; +import updater from '../../updater.js'; describe('Updater API', function () { @@ -11,53 +11,72 @@ describe('Updater API', function () { before(setup); after(cleanup); - describe('autoupdate_pattern', function () { - it('can get app auto update pattern (default)', async function () { - const response = await superagent.get(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + describe('autoupdate_config', function () { + it('can get autoupdate config (default)', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/updater/autoupdate_config`) .query({ access_token: owner.token }); assert.equal(response.status, 200); - assert.ok(response.body.pattern); + assert.ok(response.body.schedule); + assert.ok(response.body.policy); }); - it('cannot set autoupdate_pattern without pattern', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + it('cannot set autoupdate_config without schedule', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_config`) .query({ access_token: owner.token }) + .send({ policy: updater.AUTOUPDATE_POLICY_PLATFORM_AND_APPS }) .ok(() => true); assert.equal(response.status, 400); }); - it('can set autoupdate_pattern', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + it('cannot set autoupdate_config without policy', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_config`) .query({ access_token: owner.token }) - .send({ pattern: '00 30 11 * * 1-5' }); + .send({ schedule: '00 30 11 * * 1-5' }) + .ok(() => true); + assert.equal(response.status, 400); + }); + + it('can set autoupdate_config', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_config`) + .query({ access_token: owner.token }) + .send({ schedule: '00 30 11 * * 1-5', policy: updater.AUTOUPDATE_POLICY_APPS_ONLY }); assert.equal(response.status, 200); }); - it('can get auto update pattern', async function () { - const response = await superagent.get(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + it('can get autoupdate config after set', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/updater/autoupdate_config`) .query({ access_token: owner.token }); assert.equal(response.status, 200); - assert.equal(response.body.pattern, '00 30 11 * * 1-5'); + assert.equal(response.body.schedule, '00 30 11 * * 1-5'); + assert.equal(response.body.policy, updater.AUTOUPDATE_POLICY_APPS_ONLY); }); - it('can set autoupdate_pattern to never', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + it('can set autoupdate policy to never', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_config`) .query({ access_token: owner.token }) - .send({ pattern: constants.CRON_PATTERN_NEVER }); + .send({ schedule: '00 30 11 * * 1-5', policy: updater.AUTOUPDATE_POLICY_NEVER }); assert.equal(response.status, 200); }); - it('can get auto update pattern', async function () { - const response = await superagent.get(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + it('can get autoupdate config with never policy', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/updater/autoupdate_config`) .query({ access_token: owner.token }); assert.equal(response.status, 200); - assert.equal(response.body.pattern, constants.CRON_PATTERN_NEVER); + assert.equal(response.body.policy, updater.AUTOUPDATE_POLICY_NEVER); }); - it('cannot set invalid autoupdate_pattern', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_pattern`) + it('cannot set invalid autoupdate schedule', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_config`) .query({ access_token: owner.token }) - .send({ pattern: '1 3 x 5 6' }) + .send({ schedule: '1 3 x 5 6', policy: updater.AUTOUPDATE_POLICY_PLATFORM_AND_APPS }) + .ok(() => true); + assert.equal(response.status, 400); + }); + + it('cannot set invalid autoupdate policy', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/updater/autoupdate_config`) + .query({ access_token: owner.token }) + .send({ schedule: '00 30 11 * * 1-5', policy: 'invalid_policy' }) .ok(() => true); assert.equal(response.status, 400); }); diff --git a/src/routes/updater.js b/src/routes/updater.js index 126a781b8..cb9a0ce38 100644 --- a/src/routes/updater.js +++ b/src/routes/updater.js @@ -6,20 +6,20 @@ import { HttpSuccess } from '@cloudron/connect-lastmile'; import safe from 'safetydance'; import updater from '../updater.js'; - -async function getAutoupdatePattern(req, res, next) { - const [error, pattern] = await safe(updater.getAutoupdatePattern()); +async function getAutoupdateConfig(req, res, next) { + const [error, config] = await safe(updater.getAutoupdateConfig()); if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { pattern })); + next(new HttpSuccess(200, config)); } -async function setAutoupdatePattern(req, res, next) { +async function setAutoupdateConfig(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required')); + if (typeof req.body.schedule !== 'string') return next(new HttpError(400, 'schedule is required')); + if (typeof req.body.policy !== 'string') return next(new HttpError(400, 'policy is required')); - const [error] = await safe(updater.setAutoupdatePattern(req.body.pattern)); + const [error] = await safe(updater.setAutoupdateConfig(req.body)); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -54,8 +54,8 @@ async function checkBoxUpdate(req, res, next) { } export default { - getAutoupdatePattern, - setAutoupdatePattern, + getAutoupdateConfig, + setAutoupdateConfig, getBoxUpdate, checkBoxUpdate, diff --git a/src/server.js b/src/server.js index 36bc997cf..5eb96606c 100644 --- a/src/server.js +++ b/src/server.js @@ -134,8 +134,8 @@ async function initializeExpressSync() { router.get ('/api/v1/updater/box_update', token, authorizeUser, routes.updater.getBoxUpdate); // allowed for normal users to make it work for app operators router.post('/api/v1/updater/box_update', json, token, authorizeAdmin, routes.updater.updateBox); router.post('/api/v1/updater/check_box_update', json, token, authorizeAdmin, routes.updater.checkBoxUpdate); - router.get ('/api/v1/updater/autoupdate_pattern', token, authorizeAdmin, routes.updater.getAutoupdatePattern); - router.post('/api/v1/updater/autoupdate_pattern', json, token, authorizeAdmin, routes.updater.setAutoupdatePattern); + router.get ('/api/v1/updater/autoupdate_config', token, authorizeAdmin, routes.updater.getAutoupdateConfig); + router.post('/api/v1/updater/autoupdate_config', json, token, authorizeAdmin, routes.updater.setAutoupdateConfig); // task routes router.get ('/api/v1/tasks', token, authorizeAdmin, routes.tasks.list); diff --git a/src/settings.js b/src/settings.js index a740de55a..9d5a9fb6b 100644 --- a/src/settings.js +++ b/src/settings.js @@ -4,7 +4,8 @@ import safe from 'safetydance'; const APPSTORE_API_TOKEN_KEY = 'appstore_api_token'; const API_SERVER_ORIGIN_KEY = 'api_server_origin'; -const AUTOUPDATE_PATTERN_KEY = 'autoupdate_pattern'; +const AUTOUPDATE_SCHEDULE_KEY = 'autoupdate_schedule'; +const AUTOUPDATE_POLICY_KEY = 'autoupdate_policy'; const CLOUDRON_AVATAR_KEY = 'cloudron_avatar'; const CLOUDRON_BACKGROUND_KEY = 'cloudron_background'; const CLOUDRON_ID_KEY = 'cloudron_id'; @@ -95,7 +96,8 @@ export default { setBlob, APPSTORE_API_TOKEN_KEY, API_SERVER_ORIGIN_KEY, - AUTOUPDATE_PATTERN_KEY, + AUTOUPDATE_SCHEDULE_KEY, + AUTOUPDATE_POLICY_KEY, CLOUDRON_AVATAR_KEY, CLOUDRON_BACKGROUND_KEY, CLOUDRON_ID_KEY, diff --git a/src/test/updater-test.js b/src/test/updater-test.js index 3b17a8334..a961e1932 100644 --- a/src/test/updater-test.js +++ b/src/test/updater-test.js @@ -20,18 +20,24 @@ describe('updater', function () { before(setup); after(cleanup); - it('can get default autoupdate_pattern', async function () { - const pattern = await updater.getAutoupdatePattern(); - assert.equal(pattern, '00 00 1,3,5,23 * * *'); + it('can get default autoupdate config', async function () { + const config = await updater.getAutoupdateConfig(); + assert.equal(config.schedule, '00 00 1,3,5,23 * * *'); + assert.equal(config.policy, updater.AUTOUPDATE_POLICY_PLATFORM_AND_APPS); }); - it('cannot set invalid autoupdate_pattern', async function () { - const [error] = await safe(updater.setAutoupdatePattern('02 * 1 *')); + it('cannot set invalid autoupdate schedule', async function () { + const [error] = await safe(updater.setAutoupdateConfig({ schedule: '02 * 1 *', policy: updater.AUTOUPDATE_POLICY_PLATFORM_AND_APPS })); assert.equal(error.reason, BoxError.BAD_FIELD); }); - it('can set default autoupdate_pattern', async function () { - await updater.setAutoupdatePattern('02 * 1-5 * * *'); + it('cannot set invalid autoupdate policy', async function () { + const [error] = await safe(updater.setAutoupdateConfig({ schedule: '02 * 1-5 * * *', policy: 'invalid' })); + assert.equal(error.reason, BoxError.BAD_FIELD); + }); + + it('can set autoupdate config', async function () { + await updater.setAutoupdateConfig({ schedule: '02 * 1-5 * * *', policy: updater.AUTOUPDATE_POLICY_APPS_ONLY }); }); }); @@ -44,7 +50,7 @@ describe('updater', function () { before(async function () { safe.fs.unlinkSync(paths.BOX_UPDATE_FILE); - await updater.setAutoupdatePattern(constants.CRON_PATTERN_NEVER); + await updater.setAutoupdateConfig({ schedule: '00 00 1,3,5,23 * * *', policy: updater.AUTOUPDATE_POLICY_NEVER }); }); it('no updates', async function () { @@ -97,7 +103,7 @@ describe('updater', function () { describe('app updates', function () { before(async function () { - await updater.setAutoupdatePattern(constants.CRON_PATTERN_NEVER); + await updater.setAutoupdateConfig({ schedule: '00 00 1,3,5,23 * * *', policy: updater.AUTOUPDATE_POLICY_NEVER }); }); it('no updates', async function () { diff --git a/src/updater.js b/src/updater.js index 2f4f8762f..6302ece26 100644 --- a/src/updater.js +++ b/src/updater.js @@ -29,25 +29,39 @@ import tasks from './tasks.js'; const { log } = logger('updater'); const shell = shellModule('updater'); +const AUTOUPDATE_POLICY_NEVER = 'never'; +const AUTOUPDATE_POLICY_APPS_ONLY = 'apps_only'; +const AUTOUPDATE_POLICY_PLATFORM_AND_APPS = 'platform_and_apps'; const RELEASES_PUBLIC_KEY = path.join(import.meta.dirname, 'releases.gpg'); const UPDATE_CMD = path.join(import.meta.dirname, 'scripts/update.sh'); -async function setAutoupdatePattern(pattern) { - assert.strictEqual(typeof pattern, 'string'); +function validatePolicy(policy) { + if (policy === AUTOUPDATE_POLICY_NEVER || policy === AUTOUPDATE_POLICY_APPS_ONLY || policy === AUTOUPDATE_POLICY_PLATFORM_AND_APPS) return; - if (pattern !== constants.CRON_PATTERN_NEVER) { // check if pattern is valid - const job = safe.safeCall(function () { return new CronTime(pattern); }); - if (!job) throw new BoxError(BoxError.BAD_FIELD, 'Invalid pattern'); - } - - await settings.set(settings.AUTOUPDATE_PATTERN_KEY, pattern); - await cron.handleAutoupdatePatternChanged(pattern); + return new BoxError(BoxError.BAD_FIELD, 'Invalid policy'); } -async function getAutoupdatePattern() { - const pattern = await settings.get(settings.AUTOUPDATE_PATTERN_KEY); - return pattern || cron.DEFAULT_AUTOUPDATE_PATTERN; +async function setAutoupdateConfig(config) { + assert.strictEqual(typeof config, 'object'); + assert.strictEqual(typeof config.schedule, 'string'); + assert.strictEqual(typeof config.policy, 'string'); + + const error = validatePolicy(config.policy); + if (error) throw error; + + const job = safe.safeCall(function () { return new CronTime(config.schedule); }); + if (!job) throw new BoxError(BoxError.BAD_FIELD, 'Invalid schedule pattern'); + + await settings.set(settings.AUTOUPDATE_SCHEDULE_KEY, config.schedule); + await settings.set(settings.AUTOUPDATE_POLICY_KEY, config.policy); + await cron.handleAutoupdateConfigChanged(config); +} + +async function getAutoupdateConfig() { + const schedule = await settings.get(settings.AUTOUPDATE_SCHEDULE_KEY) || cron.DEFAULT_AUTOUPDATE_PATTERN; + const policy = await settings.get(settings.AUTOUPDATE_POLICY_KEY) || AUTOUPDATE_POLICY_PLATFORM_AND_APPS; + return { schedule, policy }; } async function downloadBoxUrl(url, file) { @@ -262,14 +276,19 @@ async function notifyBoxUpdate() { async function autoUpdate(auditSource) { assert.strictEqual(typeof auditSource, 'object'); - const boxUpdateInfo = await getBoxUpdate(); - // do box before app updates. for the off chance that the box logic fixes some app update logic issue - if (boxUpdateInfo && !boxUpdateInfo.unstable) { - log('autoUpdate: starting box autoupdate to %j', boxUpdateInfo.version); - const [error] = await safe(startBoxUpdateTask({ skipBackup: false }, AuditSource.CRON)); - if (!error) return; // do not start app updates when a box update got scheduled - log(`autoUpdate: failed to start box autoupdate task: ${error.message}`); - // fall through to update apps if box update never started (failed ubuntu or avx check) + const { policy } = await getAutoupdateConfig(); + if (policy === AUTOUPDATE_POLICY_NEVER) return; + + if (policy === AUTOUPDATE_POLICY_PLATFORM_AND_APPS) { + const boxUpdateInfo = await getBoxUpdate(); + // do box before app updates. for the off chance that the box logic fixes some app update logic issue + if (boxUpdateInfo && !boxUpdateInfo.unstable) { + log('autoUpdate: starting box autoupdate to %j', boxUpdateInfo.version); + const [error] = await safe(startBoxUpdateTask({ skipBackup: false }, AuditSource.CRON)); + if (!error) return; // do not start app updates when a box update got scheduled + log(`autoUpdate: failed to start box autoupdate task: ${error.message}`); + // fall through to update apps if box update never started (failed ubuntu or avx check) + } } const result = await apps.list(); @@ -329,10 +348,10 @@ async function checkBoxUpdate(options) { } async function raiseNotifications() { - const pattern = await getAutoupdatePattern(); + const { policy } = await getAutoupdateConfig(); const boxUpdate = await getBoxUpdate(); - if (pattern === constants.CRON_PATTERN_NEVER && boxUpdate) { + if (policy !== AUTOUPDATE_POLICY_PLATFORM_AND_APPS && boxUpdate) { const changelog = boxUpdate.changelog.map((m) => `* ${m}\n`).join(''); const message = `Changelog:\n${changelog}\n\nGo to the Settings view to update.\n\n`; @@ -371,8 +390,12 @@ async function checkForUpdates(options) { } export default { - setAutoupdatePattern, - getAutoupdatePattern, + AUTOUPDATE_POLICY_NEVER, + AUTOUPDATE_POLICY_APPS_ONLY, + AUTOUPDATE_POLICY_PLATFORM_AND_APPS, + + setAutoupdateConfig, + getAutoupdateConfig, startBoxUpdateTask, updateBox,