Skip to content
4 changes: 2 additions & 2 deletions forge/db/controllers/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,8 @@ module.exports = {
const days = autoStackUpdate.days
const hours = autoStackUpdate.hours
// generate random day and hour in ranges
const day = days[Math.round(days.length * Math.random())]
const hour = hours[Math.round(hours.length * Math.random())]
const day = days[Math.floor(days.length * Math.random())]
const hour = hours[Math.floor(hours.length * Math.random())]
await instance.updateSetting(`${KEY_STACK_UPGRADE_HOUR}_${day}`, { hour })
}
}
Expand Down
1 change: 1 addition & 0 deletions forge/ee/lib/autoUpdateStacks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ module.exports.init = async function (app) {
app.config.features.register('autoStackUpdate', true, true)

app.housekeeper.registerTask(require('./tasks/upgrade-stack'))
app.housekeeper.registerTask(require('./tasks/enforce-team-rules'))
}
60 changes: 60 additions & 0 deletions forge/ee/lib/autoUpdateStacks/tasks/enforce-team-rules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* If a Team changes TeamType apply new AutoUpdate rules
*/
const { KEY_STACK_UPGRADE_HOUR } = require('../../../../db/models/ProjectSettings')
const { randomInt } = require('../../../../housekeeper/utils')

module.exports = {
name: 'fixTeamStackUpdateRules',
startup: false,
schedule: `${randomInt(0, 29)} ${randomInt(0, 23)} * * *`, // random time every day
run: async function (app) {
if (app.config.features.enabled('autoStackUpdate')) {
app.log.info('Running AutoStackUpgrade Teams Tests')
const teamTypes = await app.db.models.TeamType.getAll({}, { active: true })
for (const teamType of teamTypes.types) {
if (teamType) {
const typeProperties = teamType.properties
if (typeProperties.autoStackUpdate?.allowDisable === false) {
app.log.info(`Found TeamType ${teamType.name} needs to enforce AutoStackUpdate`)
const autoStackUpdate = teamType.getProperty('autoStackUpdate')
// this TeamType needs to enforce restart rules.
// get all teams with this type, then get all instances in those teams
const teams = await app.db.models.Team.findAll({
where: {
TeamTypeId: teamType.id
}
})
for (const team of teams) {
if (team) {
const instances = await app.db.models.Project.byTeam(team.id)
for (const instance of instances) {
if (instance) {
let found = false
for (let day = 0; day < 7; day++) {
const k = await instance.getSetting(`${KEY_STACK_UPGRADE_HOUR}_${day}`)
if (k) {
found = true
break
}
}

if (!found) {
const days = autoStackUpdate.days
const hours = autoStackUpdate.hours
// generate random day and hour in ranges
const day = days[Math.floor(days.length * Math.random())]
const hour = hours[Math.floor(hours.length * Math.random())]
app.log.info(`AutoStackUpgrade: applying update schedule ${day}/${hour} to instance ${instance.id} in team ${team.hashid}`)
await instance.updateSetting(`${KEY_STACK_UPGRADE_HOUR}_${day}`, { hour })
}
}
}
}
}
}
}
}
}
}
}
129 changes: 129 additions & 0 deletions test/unit/forge/ee/lib/autoUpdateStacks/index_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const { Op } = require('sequelize')
const should = require('should') // eslint-disable-line

const { KEY_STACK_UPGRADE_HOUR } = require('../../../../../../forge/db/models/ProjectSettings')
const enforceTeamRulesTask = require('../../../../../../forge/ee/lib/autoUpdateStacks/tasks/enforce-team-rules')
const setup = require('../../setup')

describe('Automatic Stack Upgrade', function () {
let app

before(async function () {
setup.setupStripe()
app = await setup()

const defaultTeamType = await app.db.models.TeamType.findOne({ where: { id: 1 } })

const autoTeamTypeProperties = {
name: 'AutoUpdate',
order: 2,
description: 'TeamType that forces Stack Updates',
properties: {
users: {
limit: 10
},
runtimes: {
limit: 20
},
devices: {
productId: 'prod_device',
priceId: 'price_device',
description: '$5/month',
limit: 10
},
billing: {
productId: 'prod_team',
priceId: 'price_team',
description: '$10/month',
proration: 'always_invoice'
},
trial: {
active: false
},
enableAllFeatures: true,
features: {
fileStorageLimit: null,
contextLimit: null
},
teamBroker: {
clients: {
limit: 10
}
},
autoStackUpdate: {
enabled: true,
days: [0, 6],
hours: [0, 1, 2, 3, 4],
allowDisable: false
}
}
}

autoTeamTypeProperties.instances = defaultTeamType.properties.instances

const autoTeamType = await app.db.models.TeamType.create(autoTeamTypeProperties)
app.autoTeamType = autoTeamType
})

after(async function () {
if (app) {
await app.close()
app = null
}
setup.resetStripe()
})

describe('Update after TeamType change', async function () {
it('Instance properties updated', async function () {
const instanceStartSettings = await app.db.models.ProjectSettings.findAll({
where: {
ProjectId: app.instance.id,
key: {
[Op.like]: `${KEY_STACK_UPGRADE_HOUR}_%`
}
}
})
instanceStartSettings.should.have.length(0)
await app.team.updateTeamType(app.autoTeamType, { interval: 'month' })
await enforceTeamRulesTask.run(app)
const instanceAfterSettings = await app.db.models.ProjectSettings.findAll({
where: {
ProjectId: app.instance.id,
key: {
[Op.like]: `${KEY_STACK_UPGRADE_HOUR}_%`
}
}
})
instanceAfterSettings.should.have.length(1)
instanceAfterSettings[0].value.hour.should.be.oneOf([0, 1, 2, 3, 4])
const day = parseInt(instanceAfterSettings[0].key.split('_')[1])
day.should.be.oneOf([0, 6])
})
it('Existing Instance properties not updated', async function () {
// depends on previous test
const instanceStartSettings = await app.db.models.ProjectSettings.findAll({
where: {
ProjectId: app.instance.id,
key: {
[Op.like]: `${KEY_STACK_UPGRADE_HOUR}_%`
}
}
})
instanceStartSettings.should.have.length(1)
const day = parseInt(instanceStartSettings[0].key.split('_')[1])
const hour = instanceStartSettings[0].value.hour
await enforceTeamRulesTask.run(app)
const instanceAfterSettings = await app.db.models.ProjectSettings.findAll({
where: {
ProjectId: app.instance.id,
key: {
[Op.like]: `${KEY_STACK_UPGRADE_HOUR}_%`
}
}
})
instanceAfterSettings.should.have.length(1)
day.should.equal(parseInt(instanceAfterSettings[0].key.split('_')[1]))
hour.should.equal(instanceAfterSettings[0].value.hour)
})
})
})
Loading