Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support disabling instance launcher "auto safe mode" #4922

Merged
merged 12 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions forge/db/models/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const { col, fn, DataTypes, Op, where } = require('sequelize')

const Controllers = require('../controllers')

const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL, KEY_CUSTOM_HOSTNAME } = require('./ProjectSettings')
const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL, KEY_CUSTOM_HOSTNAME, KEY_DISABLE_AUTO_SAFE_MODE } = require('./ProjectSettings')

const BANNED_NAME_LIST = [
'app',
Expand Down Expand Up @@ -408,7 +408,8 @@ module.exports = {
{ key: KEY_HA },
{ key: KEY_PROTECTED },
{ key: KEY_CUSTOM_HOSTNAME },
{ key: KEY_HEALTH_CHECK_INTERVAL }
{ key: KEY_HEALTH_CHECK_INTERVAL },
{ key: KEY_DISABLE_AUTO_SAFE_MODE }
]
},
required: false
Expand Down
2 changes: 2 additions & 0 deletions forge/db/models/ProjectSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const KEY_PROTECTED = 'protected'
const KEY_HEALTH_CHECK_INTERVAL = 'healthCheckInterval'
const KEY_CUSTOM_HOSTNAME = 'customHostname'
const KEY_SHARED_ASSETS = 'sharedAssets'
const KEY_DISABLE_AUTO_SAFE_MODE = 'disableAutoSafeMode'

module.exports = {
KEY_SETTINGS,
Expand All @@ -25,6 +26,7 @@ module.exports = {
KEY_HEALTH_CHECK_INTERVAL,
KEY_CUSTOM_HOSTNAME,
KEY_SHARED_ASSETS,
KEY_DISABLE_AUTO_SAFE_MODE,
name: 'ProjectSettings',
schema: {
ProjectId: { type: DataTypes.UUID, unique: 'pk_settings' },
Expand Down
10 changes: 8 additions & 2 deletions forge/db/views/Project.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL, KEY_CUSTOM_HOSTNAME } = require('../models/ProjectSettings')
const { KEY_HOSTNAME, KEY_SETTINGS, KEY_HA, KEY_PROTECTED, KEY_HEALTH_CHECK_INTERVAL, KEY_CUSTOM_HOSTNAME, KEY_DISABLE_AUTO_SAFE_MODE } = require('../models/ProjectSettings')

module.exports = function (app) {
app.addSchema({
Expand Down Expand Up @@ -37,7 +37,8 @@ module.exports = function (app) {
launcherSettings: {
type: 'object',
properties: {
healthCheckInterval: { type: 'number' }
healthCheckInterval: { type: 'number' },
disableAutoSafeMode: { type: 'boolean' }
},
additionalProperties: false
}
Expand Down Expand Up @@ -75,6 +76,11 @@ module.exports = function (app) {
result.launcherSettings = {}
result.launcherSettings.healthCheckInterval = heathCheckIntervalRow?.value
}
const disableAutoSafeMode = proj.ProjectSettings?.find((projectSettingsRow) => projectSettingsRow.key === KEY_DISABLE_AUTO_SAFE_MODE)
if (typeof disableAutoSafeMode?.value === 'boolean') {
result.launcherSettings = result.launcherSettings || {}
result.launcherSettings.disableAutoSafeMode = disableAutoSafeMode.value
}
// Environment
result.settings.env = app.db.controllers.Project.insertPlatformSpecificEnvVars(proj, result.settings.env)
if (!result.settings.palette?.modules) {
Expand Down
32 changes: 23 additions & 9 deletions forge/routes/api/project.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { KEY_SETTINGS, KEY_HEALTH_CHECK_INTERVAL, KEY_SHARED_ASSETS } = require('../../db/models/ProjectSettings')
const { KEY_SETTINGS, KEY_HEALTH_CHECK_INTERVAL, KEY_DISABLE_AUTO_SAFE_MODE, KEY_SHARED_ASSETS } = require('../../db/models/ProjectSettings')
const { Roles } = require('../../lib/roles')

const ProjectActions = require('./projectActions')
Expand Down Expand Up @@ -456,15 +456,24 @@ module.exports = async function (app) {
}

// Launcher settings
if (request.body?.launcherSettings?.healthCheckInterval) {
const oldInterval = await request.project.getSetting(KEY_HEALTH_CHECK_INTERVAL)
const newInterval = parseInt(request.body.launcherSettings.healthCheckInterval, 10)
if (isNaN(newInterval) || newInterval < 5000) {
reply.code(400).send({ code: 'invalid_heathCheckInterval', error: 'Invalid heath check interval' })
return
if (request.body?.launcherSettings) {
if (request.body.launcherSettings.healthCheckInterval) {
const oldInterval = await request.project.getSetting(KEY_HEALTH_CHECK_INTERVAL)
const newInterval = parseInt(request.body.launcherSettings.healthCheckInterval, 10)
if (isNaN(newInterval) || newInterval < 5000) {
reply.code(400).send({ code: 'invalid_heathCheckInterval', error: 'Invalid heath check interval' })
return
}
if (oldInterval !== newInterval) {
changesToPersist.healthCheckInterval = { from: oldInterval, to: newInterval }
}
}
if (oldInterval !== newInterval) {
changesToPersist.healthCheckInterval = { from: oldInterval, to: newInterval }
if (typeof request.body.launcherSettings.disableAutoSafeMode === 'boolean') {
const oldInterval = await request.project.getSetting(KEY_DISABLE_AUTO_SAFE_MODE)
const newInterval = request.body.launcherSettings.disableAutoSafeMode
if (oldInterval !== newInterval) {
changesToPersist.disableAutoSafeMode = { from: oldInterval, to: newInterval }
}
}
}

Expand Down Expand Up @@ -529,6 +538,10 @@ module.exports = async function (app) {
await request.project.updateSetting(KEY_HEALTH_CHECK_INTERVAL, changesToPersist.healthCheckInterval.to, { transaction })
updates.pushDifferences({ healthCheckInterval: changesToPersist.healthCheckInterval.from }, { healthCheckInterval: changesToPersist.healthCheckInterval.to })
}
if (changesToPersist.disableAutoSafeMode) {
await request.project.updateSetting(KEY_DISABLE_AUTO_SAFE_MODE, changesToPersist.disableAutoSafeMode.to, { transaction })
updates.pushDifferences({ disableAutoSafeMode: changesToPersist.disableAutoSafeMode.from }, { disableAutoSafeMode: changesToPersist.disableAutoSafeMode.to })
}

await transaction.commit() // all good, commit the transaction

Expand Down Expand Up @@ -802,6 +815,7 @@ module.exports = async function (app) {
settings.state = request.project.state
settings.stack = request.project.ProjectStack?.properties || {}
settings.healthCheckInterval = await request.project.getSetting(KEY_HEALTH_CHECK_INTERVAL)
settings.disableAutoSafeMode = await request.project.getSetting(KEY_DISABLE_AUTO_SAFE_MODE)
settings.settings = await app.db.controllers.Project.getRuntimeSettings(request.project)
if (settings.settings.env) {
settings.env = Object.assign({}, settings.settings.env, settings.env)
Expand Down
40 changes: 35 additions & 5 deletions frontend/src/pages/instance/Settings/LauncherSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
Flows that perform CPU intensive work may need to increase this from the default of 7500ms.
</template>
</FormRow>
<FormRow v-if="launcherSupportsAutoSafeMode" v-model="input.disableAutoSafeMode" type="checkbox">
Disable Auto Safe Mode
<template #description>
Prevent Node-RED from automatically entering safe mode when a crash loop is detected.
WARNING: Disabling Auto Safe Mode is not recommended. A problem that causes Node-RED to crash multiple successive times may result in a contineous bootloop that will need to be manually resolved.
</template>
</FormRow>
hardillb marked this conversation as resolved.
Show resolved Hide resolved

<div class="space-x-4 whitespace-nowrap">
<ff-button size="small" :disabled="!unsavedChanges || !validateFormInputs()" data-action="save-settings" @click="saveSettings()">Save settings</ff-button>
Expand All @@ -17,6 +24,7 @@

<script>

import SemVer from 'semver'
import { useRouter } from 'vue-router'

import { mapState } from 'vuex'
Expand Down Expand Up @@ -46,10 +54,12 @@ export default {
return {
mounted: false,
original: {
healthCheckInterval: null
healthCheckInterval: null,
disableAutoSafeMode: null
},
input: {
healthCheckInterval: null
healthCheckInterval: null,
disableAutoSafeMode: null
},
errors: {
healthCheckInterval: ''
Expand All @@ -60,7 +70,17 @@ export default {
computed: {
...mapState('account', ['team', 'teamMembership']),
unsavedChanges: function () {
return this.original.healthCheckInterval !== this.input.healthCheckInterval
return +this.original.healthCheckInterval !== +this.input.healthCheckInterval ||
this.original.disableAutoSafeMode !== this.input.disableAutoSafeMode
},
launcherSupportsAutoSafeMode: function () {
const launcherVersion = this.project?.meta?.versions?.launcher
if (!launcherVersion) {
// We won't have this for a suspended project - so err on the side
// of permissive
return true
}
return SemVer.satisfies(SemVer.coerce(launcherVersion), '>=2.12.0')
}
},
watch: {
Expand All @@ -69,6 +89,11 @@ export default {
if (this.mounted) {
this.validateFormInputs()
}
},
'input.disableAutoSafeMode': function (value) {
if (this.mounted) {
this.validateFormInputs()
}
}
},
mounted () {
Expand All @@ -86,7 +111,7 @@ export default {
if (!this.unsavedChanges) {
this.errors.healthCheckInterval = ''
} else {
const hci = parseInt(this.input.healthCheckInterval)
const hci = +this.input.healthCheckInterval
if (isNaN(hci) || hci < 5000) {
this.errors.healthCheckInterval = 'Health check interval must be 5000 or greater'
} else {
Expand All @@ -98,10 +123,15 @@ export default {
getSettings: function () {
this.original.healthCheckInterval = this.project?.launcherSettings?.healthCheckInterval
this.input.healthCheckInterval = this.project?.launcherSettings.healthCheckInterval
this.original.disableAutoSafeMode = this.project?.launcherSettings?.disableAutoSafeMode ?? false
this.input.disableAutoSafeMode = this.project?.launcherSettings.disableAutoSafeMode ?? false
},
async saveSettings () {
const launcherSettings = {
healthCheckInterval: this.input.healthCheckInterval
healthCheckInterval: +this.input.healthCheckInterval
}
if (this.launcherSupportsAutoSafeMode) {
launcherSettings.disableAutoSafeMode = this.input.disableAutoSafeMode
}
if (!this.validateFormInputs()) {
alerts.emit('Please correct the errors before saving.', 'error')
Expand Down
13 changes: 8 additions & 5 deletions test/unit/forge/routes/api/project_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const setup = require('../setup')

const FF_UTIL = require('flowforge-test-utils')
const { Roles } = FF_UTIL.require('forge/lib/roles')
const { KEY_HEALTH_CHECK_INTERVAL } = FF_UTIL.require('forge/db/models/ProjectSettings')
const { KEY_HEALTH_CHECK_INTERVAL, KEY_DISABLE_AUTO_SAFE_MODE } = FF_UTIL.require('forge/db/models/ProjectSettings')
const { START_DELAY, STOP_DELAY } = FF_UTIL.require('forge/containers/stub/index.js')

describe('Project API', function () {
Expand Down Expand Up @@ -1691,7 +1691,7 @@ describe('Project API', function () {
{ name: 'two', value: '2' }
]) // should be unchanged
})
it('Change launcher health check interval - owner', async function () {
it('Change launcher settings - owner', async function () {
// Setup some flows/credentials
await addFlowsToProject(app,
TestObjects.project1.id,
Expand All @@ -1708,15 +1708,18 @@ describe('Project API', function () {
url: `/api/v1/projects/${TestObjects.project1.id}`,
payload: {
launcherSettings: {
healthCheckInterval: 9876
healthCheckInterval: 9876,
disableAutoSafeMode: true
}
},
cookies: { sid: TestObjects.tokens.alice }
})
response.statusCode.should.equal(200)

const newValue = await TestObjects.project1.getSetting(KEY_HEALTH_CHECK_INTERVAL)
should(newValue).equal(9876)
const healthValue = await TestObjects.project1.getSetting(KEY_HEALTH_CHECK_INTERVAL)
should(healthValue).equal(9876)
const safeModeValue = await TestObjects.project1.getSetting(KEY_DISABLE_AUTO_SAFE_MODE)
should(safeModeValue).equal(true)
})
it('Change launcher health check interval bad value - owner', async function () {
// Setup some flows/credentials
Expand Down
Loading