Skip to content

Commit 0ba1917

Browse files
authored
Merge pull request #6408 from FlowFuse/scheduled-instance-restarts
Enable schedule instance restart
2 parents 08a6c63 + d17b872 commit 0ba1917

File tree

4 files changed

+73
-44
lines changed

4 files changed

+73
-44
lines changed

forge/db/models/ProjectSettings.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* A Project Settings model
33
* @namespace forge.db.models.ProjectSettings
44
*/
5-
const { DataTypes } = require('sequelize')
5+
const { DataTypes, Op } = require('sequelize')
66

77
const SettingTypes = {
88
STRING: 0,
@@ -76,7 +76,10 @@ module.exports = {
7676
getProjectsToUpgrade: async (hour, day) => {
7777
return await this.findAll({
7878
where: {
79-
key: `${KEY_STACK_UPGRADE_HOUR}_${day}`, value: `${JSON.stringify({ hour })}`
79+
key: `${KEY_STACK_UPGRADE_HOUR}_${day}`,
80+
value: {
81+
[Op.like]: `${JSON.stringify({ hour }).slice(0, -1)},%`
82+
}
8083
},
8184
include: {
8285
model: M.Project,

forge/ee/lib/autoUpdateStacks/tasks/upgrade-stack.js

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,30 @@ module.exports = {
1818
const projectList = await app.db.models.ProjectSettings.getProjectsToUpgrade(hour, day)
1919
if (projectList) {
2020
for (const project of projectList) {
21-
if (project.value.restartOnly) { // this might need to be a separate flag to make the query work
21+
// we should probably rate limit this to not restart lots of projects at once
22+
if (project.Project.ProjectStack.replacedBy) {
23+
// need to add audit logging
24+
try {
25+
const newStack = await app.db.models.ProjectStack.byId(project.Project.ProjectStack.replacedBy)
26+
app.log.info(`Updating project ${project.Project.id} to stack: '${newStack.hashid}'`)
27+
28+
const suspendOptions = {
29+
skipBilling: true
30+
}
31+
32+
app.db.controllers.Project.setInflightState(project.Project, 'starting')
33+
const result = await suspendProject(project.Project, suspendOptions)
34+
35+
await project.Project.setProjectStack(newStack)
36+
await project.Project.save()
37+
38+
await app.auditLog.Project.project.stack.changed(null, null, project.Project, newStack)
39+
40+
await unSuspendProject(project.Project, result.resumeProject, result.targetState)
41+
} catch (err) {
42+
app.log.info(`Problem updating project ${project.Project.id} - ${err.toString()}`)
43+
}
44+
} else if (project.value.restart) {
2245
try {
2346
app.log.info(`Restarting project ${project.Project.id} as scheduled`)
2447
await app.db.controllers.Project.setInflightState(project.Project, 'restarting')
@@ -30,31 +53,6 @@ module.exports = {
3053
} catch (err) {
3154
app.log.info(`Problem restarting project ${project.Project.id} - ${err.toString()}`)
3255
}
33-
} else {
34-
// we should probably rate limit this to not restart lots of projects at once
35-
if (project.Project.ProjectStack.replacedBy) {
36-
// need to add audit logging
37-
try {
38-
const newStack = await app.db.models.ProjectStack.byId(project.Project.ProjectStack.replacedBy)
39-
app.log.info(`Updating project ${project.Project.id} to stack: '${newStack.hashid}'`)
40-
41-
const suspendOptions = {
42-
skipBilling: true
43-
}
44-
45-
app.db.controllers.Project.setInflightState(project.Project, 'starting')
46-
const result = await suspendProject(project.Project, suspendOptions)
47-
48-
await project.Project.setProjectStack(newStack)
49-
await project.Project.save()
50-
51-
await app.auditLog.Project.project.stack.changed(null, null, project.Project, newStack)
52-
53-
await unSuspendProject(project.Project, result.resumeProject, result.targetState)
54-
} catch (err) {
55-
app.log.info(`Problem updating project ${project.Project.id} - ${err.toString()}`)
56-
}
57-
}
5856
}
5957
}
6058
}

forge/ee/routes/autoUpdateStacks/index.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ module.exports = async function (app) {
5252
type: 'object',
5353
properties: {
5454
hour: { type: 'number' },
55-
day: { type: 'number' }
55+
day: { type: 'number' },
56+
restart: { type: 'boolean' }
5657
}
5758
}
5859
},
@@ -101,7 +102,8 @@ module.exports = async function (app) {
101102
type: 'object',
102103
properties: {
103104
hour: { type: 'number' },
104-
day: { type: 'number' }
105+
day: { type: 'number' },
106+
restart: { type: 'boolean' }
105107
}
106108
}
107109
}
@@ -114,7 +116,8 @@ module.exports = async function (app) {
114116
type: 'object',
115117
properties: {
116118
hour: { type: 'number' },
117-
day: { type: 'number' }
119+
day: { type: 'number' },
120+
restart: { type: 'boolean' }
118121
}
119122
}
120123
},
@@ -130,7 +133,7 @@ module.exports = async function (app) {
130133
}
131134
try {
132135
for (const d of request.body.schedule) {
133-
await request.project.updateSetting(`${KEY_STACK_UPGRADE_HOUR}_${d.day}`, { hour: d.hour })
136+
await request.project.updateSetting(`${KEY_STACK_UPGRADE_HOUR}_${d.day}`, { hour: d.hour, restart: d.restart })
134137
}
135138
} catch (err) {
136139
return reply

frontend/src/pages/instance/Settings/Maintenance.vue

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22
<div class="maintenance">
33
<section data-el="scheduled-upgrade" class="scheduled-upgrade">
44
<FeatureUnavailable v-if="!isInstanceAutoStackUpdateFeatureEnabledForPlatform" />
5-
<FormHeading>Scheduled Upgrades</FormHeading>
5+
<FormHeading>Scheduled Restarts/Upgrades</FormHeading>
66
<FormRow v-model="scheduledUpgrade.enabled" :disabled="!allowDisable" type="checkbox" class="mt-5" container-class="max-w-xl">
7-
Apply upgrades when available
7+
Enabled
88
<template #description>
9-
<p>
10-
Select the day of the week and the hour during which the automatic upgrade will occur if available. The
11-
upgrade will start within the selected hour.
12-
</p>
13-
<p class="my-3"><span class="font-bold">Note:</span> All times are stated in UTC.</p>
9+
Select the days of the week and the hour during which the automatic upgrade will occur if available. The upgrade will start within the selected hour.
1410
</template>
1511
</FormRow>
12+
<FormRow v-model="scheduledUpgrade.restart" :disabled="!scheduledUpgrade.enabled" type="checkbox">
13+
Restart even if no update available
14+
<template #description>
15+
This will trigger a Node-RED restart even if no update is available.
16+
</template>
17+
</FormRow>
18+
<p class="my-3"><span class="font-bold">Note:</span> All times are stated in UTC.</p>
1619
<div class="my-5 flex flex-col gap-5 max-w-xl">
1720
<!-- <pre>{{ scheduledUpgrade }}</pre> -->
1821
<ul class="days-selector flex flex-row flex-wrap justify-start gap-3">
@@ -84,6 +87,7 @@ export default {
8487
return {
8588
scheduledUpgrade: {
8689
enabled: false,
90+
restart: false,
8791
startHour: null,
8892
selectedWeekdays: [],
8993
initialValue: null
@@ -220,7 +224,8 @@ export default {
220224
.then(response => {
221225
this.scheduledUpgrade.initialValue = {
222226
days: response.map(entry => entry.day),
223-
hour: response[0].hour // use the first entry hour, they should all be the same
227+
hour: response[0].hour, // use the first entry hour, they should all be the same
228+
restart: response[0].restart // use the first entry restart, they should all be the same
224229
}
225230
this.scheduledUpgrade.initialValue.enabled = true
226231
@@ -231,23 +236,33 @@ export default {
231236
seconds: 0
232237
}
233238
this.scheduledUpgrade.enabled = true
239+
this.scheduledUpgrade.restart = response[0].restart // use the first entry restart, they should all be the same
234240
}).catch(error => {
235241
if (error.response.status === 404) {
236242
// Apply any defaults from the team type
237243
if (this.team.type.properties.autoStackUpdate?.days?.length > 0 && this.team.type.properties.autoStackUpdate?.hours?.length > 0) {
238-
this.scheduledUpgrade.initialValue = { enabled: false }
244+
this.scheduledUpgrade.initialValue = {
245+
enabled: false,
246+
restart: false
247+
}
239248
this.scheduledUpgrade.selectedWeekdays = [...this.team.type.properties.autoStackUpdate.days]
249+
this.scheduledUpgrade.initialValue.days = [...this.team.type.properties.autoStackUpdate.days]
240250
this.scheduledUpgrade.startHour = {
241251
hours: this.team.type.properties.autoStackUpdate.hours[Math.round(this.team.type.properties.autoStackUpdate.hours.length * Math.random())],
242252
minutes: 0,
243253
seconds: 0
244254
}
255+
this.scheduledUpgrade.initialValue.hour = this.scheduledUpgrade.startHour.hours
245256
} else {
246257
this.scheduledUpgrade.initialValue = {
247258
enabled: false,
248259
days: []
249260
}
250261
this.scheduledUpgrade.startHour = null
262+
this.scheduledUpgrade.initialValue = {
263+
enabled: false,
264+
restart: false
265+
}
251266
}
252267
return
253268
}
@@ -263,12 +278,17 @@ export default {
263278
264279
changes.push(daysChanged)
265280
changes.push(hoursChanged)
266-
if (this.scheduledUpgrade.initialValue?.enabled && this.scheduledUpgrade?.enabled === false) {
281+
if (this.scheduledUpgrade.initialValue?.enabled !== this.scheduledUpgrade?.enabled) {
267282
// allow users to disable the schedule
268283
this.unsavedChanges = true
269284
return
270285
}
271286
287+
if (this.scheduledUpgrade.restart !== this.scheduledUpgrade.initialValue.restart) {
288+
this.unsavedChanges = true
289+
return
290+
}
291+
272292
if (mandatoryValues) {
273293
// disables the save button if values are missing
274294
this.unsavedChanges = false
@@ -281,14 +301,17 @@ export default {
281301
if (this.scheduledUpgrade.enabled) {
282302
const schedule = this.scheduledUpgrade.selectedWeekdays.map(day => ({
283303
hour: this.scheduledUpgrade.startHour.hours,
284-
day
304+
day,
305+
restart: this.scheduledUpgrade.restart
285306
}))
286307
return instanceApi.setUpdateSchedule(this.project.id, schedule)
287308
.then(() => {
288309
this.scheduledUpgrade.initialValue = {
289310
days: [...this.scheduledUpgrade.selectedWeekdays],
290-
hour: this.scheduledUpgrade.startHour.hours
311+
hour: this.scheduledUpgrade.startHour.hours,
312+
restart: this.scheduledUpgrade.restart
291313
}
314+
this.scheduledUpgrade.initialValue.enabled = true
292315
Alerts.emit('Schedule updated', 'confirmation')
293316
}).catch(error => {
294317
Alerts.emit('Failed to update schedule.', 'warning')
@@ -300,6 +323,8 @@ export default {
300323
this.scheduledUpgrade.initialValue = null
301324
this.scheduledUpgrade.selectedWeekdays = null
302325
this.scheduledUpgrade.startHour = null
326+
this.scheduledUpgrade.restart = false
327+
this.scheduledUpgrade.initialValue.enabled = false
303328
Alerts.emit('Schedule removed', 'confirmation')
304329
}).catch(error => {
305330
Alerts.emit('Failed to remove schedule', 'warning')

0 commit comments

Comments
 (0)