Skip to content

Commit

Permalink
Merge branch 'docs-docker-instal-refactor' of github.com:FlowFuse/flo…
Browse files Browse the repository at this point in the history
…wfuse into docs-docker-instal-reorder
  • Loading branch information
ppawlowski committed Oct 21, 2024
2 parents 7ab8e14 + ae68a1d commit d1e8ec3
Show file tree
Hide file tree
Showing 21 changed files with 899 additions and 159 deletions.
5 changes: 5 additions & 0 deletions forge/auditLog/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ module.exports = {
},
async membersChanged (actionedBy, error, application, deviceGroup, updates, info) {
await log('application.deviceGroup.members.changed', actionedBy, application?.id, generateBody({ error, application, deviceGroup, updates, info }))
},
settings: {
async updated (actionedBy, error, application, deviceGroup, updates) {
await log('application.deviceGroup.settings.updated', actionedBy, application?.id, generateBody({ error, application, deviceGroup, updates }))
}
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions forge/db/migrations/20241016-01-add-settings-to-devicegroup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Add settings to DeviceGroups table
*/
const { DataTypes } = require('sequelize')

module.exports = {
/**
* upgrade database
* @param {QueryInterface} context Sequelize.QueryInterface
*/
up: async (context) => {
await context.addColumn('DeviceGroups', 'settings', {
type: DataTypes.TEXT,
defaultValue: null
})
},
down: async (context) => {
}
}
23 changes: 21 additions & 2 deletions forge/db/models/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,36 @@ module.exports = {
})
},
async updateSettingsHash (settings) {
const _settings = settings || await this.getAllSettings()
const _settings = settings || await this.getAllSettings({ mergeDeviceGroupSettings: true })
delete _settings.autoSnapshot // autoSnapshot is not part of the settings hash
this.settingsHash = hashSettings(_settings)
},
async getAllSettings () {
async getAllSettings (options = { mergeDeviceGroupSettings: false }) {
const mergeDeviceGroupSettings = options.mergeDeviceGroupSettings || false
const result = {}
const settings = await this.getDeviceSettings()
settings.forEach(setting => {
result[setting.key] = setting.value
})
result.env = Controllers.Device.insertPlatformSpecificEnvVars(this, result.env) // add platform specific device env vars
// if the device is a group member, we need to merge the group settings
if (mergeDeviceGroupSettings && this.DeviceGroupId) {
const group = this.DeviceGroup || await M.DeviceGroup.byId(this.DeviceGroupId)
if (group) {
const groupEnv = await group.settings.env || []
// Merge rule: If the device has an env var AND it has a value, it remains unchanged.
// Otherwise, the value is taken from the group.
// This is to allow the device to override a (global) group env setting.
groupEnv.forEach(env => {
const existing = result.env.find(e => e.name === env.name)
if (!existing) {
result.env.push(env)
} else if (existing && !existing.value) {
existing.value = env.value
}
})
}
}
if (!Object.prototype.hasOwnProperty.call(result, 'autoSnapshot')) {
result.autoSnapshot = DEFAULT_SETTINGS.autoSnapshot
}
Expand Down
303 changes: 172 additions & 131 deletions forge/db/models/DeviceGroup.js
Original file line number Diff line number Diff line change
@@ -1,131 +1,172 @@
/**
* A DeviceGroup.
* A logical grouping of devices for the primary intent of group deployments in the pipeline stages.
* @namespace forge.db.models.DeviceGroup
*/

const { DataTypes, literal } = require('sequelize')

const { buildPaginationSearchClause } = require('../utils')
const nameValidator = { msg: 'Device Group name cannot be empty' }

module.exports = {
name: 'DeviceGroup',
schema: {
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: nameValidator,
notNull: nameValidator
}
},
description: { type: DataTypes.TEXT },
targetSnapshotId: { type: DataTypes.INTEGER, allowNull: true }
},
associations: function (M) {
this.belongsTo(M.Application, { onDelete: 'CASCADE' })
this.belongsTo(M.ProjectSnapshot, { as: 'targetSnapshot' })
this.hasMany(M.Device)
},
finders: function (M) {
const self = this
const deviceCountLiteral = literal(`(
SELECT COUNT(*)
FROM "Devices" AS "device"
WHERE
"device"."DeviceGroupId" = "DeviceGroup"."id"
AND
"device"."ApplicationId" = "DeviceGroup"."ApplicationId"
)`)
return {
static: {
byId: async function (id) {
if (typeof id === 'string') {
id = M.DeviceGroup.decodeHashid(id)
}
// find one, include application, devices and device count
return self.findOne({
where: { id },
include: [
{ model: M.Application, attributes: ['hashid', 'id', 'name', 'TeamId'] },
{
model: M.Device,
attributes: ['hashid', 'id', 'name', 'type', 'TeamId', 'ApplicationId', 'ProjectId', 'ownerType'],
where: {
ApplicationId: literal('"Devices"."ApplicationId" = "Application"."id"')
},
required: false
},
{
model: M.ProjectSnapshot,
as: 'targetSnapshot',
attributes: ['hashid', 'id', 'name']
}
],
attributes: {
include: [
[
deviceCountLiteral,
'deviceCount'
]
]
}
})
},
getAll: async (pagination = {}, where = {}) => {
const limit = parseInt(pagination.limit) || 1000
if (pagination.cursor) {
pagination.cursor = M.DeviceGroup.decodeHashid(pagination.cursor)
}
if (where.ApplicationId && typeof where.ApplicationId === 'string') {
where.ApplicationId = M.Application.decodeHashid(where.ApplicationId)
}
const [rows, count] = await Promise.all([
this.findAll({
where: buildPaginationSearchClause(pagination, where, ['DeviceGroup.name', 'DeviceGroup.description']),
include: [
{
model: M.ProjectSnapshot,
as: 'targetSnapshot',
attributes: ['hashid', 'id', 'name']
}
],
attributes: {
include: [
[
deviceCountLiteral,
'deviceCount'
]
]
},
order: [['id', 'ASC']],
limit
}),
this.count({ where })
])
return {
meta: {
next_cursor: rows.length === limit ? rows[rows.length - 1].hashid : undefined
},
count,
groups: rows
}
}
},
instance: {
deviceCount: async function () {
return await M.Device.count({ where: { DeviceGroupId: this.id, ApplicationId: this.ApplicationId } })
},
getDevices: async function () {
return await M.Device.findAll({
where: {
DeviceGroupId: this.id,
ApplicationId: this.ApplicationId
}
})
}
}
}
}
}
/**
* A DeviceGroup.
* A logical grouping of devices for the primary intent of group deployments in the pipeline stages.
* @namespace forge.db.models.DeviceGroup
*/

const { DataTypes, literal } = require('sequelize')

const { buildPaginationSearchClause } = require('../utils')
const nameValidator = { msg: 'Device Group name cannot be empty' }

module.exports = {
name: 'DeviceGroup',
schema: {
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: nameValidator,
notNull: nameValidator
}
},
description: { type: DataTypes.TEXT },
targetSnapshotId: { type: DataTypes.INTEGER, allowNull: true },
settings: {
type: DataTypes.TEXT,
set (value) {
this.setDataValue('settings', JSON.stringify(value))
},
get () {
const rawValue = this.getDataValue('settings') || '{}'
return JSON.parse(rawValue)
}
}
},
associations: function (M) {
this.belongsTo(M.Application, { onDelete: 'CASCADE' })
this.belongsTo(M.ProjectSnapshot, { as: 'targetSnapshot' })
this.hasMany(M.Device)
},
hooks: function (M, app) {
return {
afterUpdate: async (deviceGroup, options) => {
// if `settings` is updated, we need to update the settings hash
// of any member devices
if (deviceGroup.changed('settings')) {
const include = [
{ model: M.ProjectSnapshot, as: 'targetSnapshot', attributes: ['id', 'hashid', 'name'] },
{
model: M.DeviceGroup,
attributes: ['hashid', 'id', 'ApplicationId', 'settings']
},
{ model: M.Application, attributes: ['hashid', 'id', 'name', 'links'] }
]
const where = {
DeviceGroupId: deviceGroup.id,
ApplicationId: deviceGroup.ApplicationId
}
const devices = await M.Device.findAll({ where, include })
const updateAndSave = async (device) => {
await device.updateSettingsHash()
await device.save({
hooks: false, // skip the afterSave hook for device model (we have only updated the settings hash)
transaction: options?.transaction // pass the transaction (if any)
})
}
await Promise.all(devices.map(updateAndSave))
}
}
}
},
finders: function (M) {
const self = this
const deviceCountLiteral = literal(`(
SELECT COUNT(*)
FROM "Devices" AS "device"
WHERE
"device"."DeviceGroupId" = "DeviceGroup"."id"
AND
"device"."ApplicationId" = "DeviceGroup"."ApplicationId"
)`)
return {
static: {
byId: async function (id) {
if (typeof id === 'string') {
id = M.DeviceGroup.decodeHashid(id)
}
// find one, include application, devices and device count
return self.findOne({
where: { id },
include: [
{ model: M.Application, attributes: ['hashid', 'id', 'name', 'TeamId'] },
{
model: M.Device,
attributes: ['hashid', 'id', 'name', 'type', 'TeamId', 'ApplicationId', 'ProjectId', 'ownerType'],
where: {
ApplicationId: literal('"Devices"."ApplicationId" = "Application"."id"')
},
required: false
},
{
model: M.ProjectSnapshot,
as: 'targetSnapshot',
attributes: ['hashid', 'id', 'name']
}
],
attributes: {
include: [
[
deviceCountLiteral,
'deviceCount'
]
]
}
})
},
getAll: async (pagination = {}, where = {}) => {
const limit = parseInt(pagination.limit) || 1000
if (pagination.cursor) {
pagination.cursor = M.DeviceGroup.decodeHashid(pagination.cursor)
}
if (where.ApplicationId && typeof where.ApplicationId === 'string') {
where.ApplicationId = M.Application.decodeHashid(where.ApplicationId)
}
const [rows, count] = await Promise.all([
this.findAll({
where: buildPaginationSearchClause(pagination, where, ['DeviceGroup.name', 'DeviceGroup.description']),
include: [
{
model: M.ProjectSnapshot,
as: 'targetSnapshot',
attributes: ['hashid', 'id', 'name']
}
],
attributes: {
include: [
[
deviceCountLiteral,
'deviceCount'
]
]
},
order: [['id', 'ASC']],
limit
}),
this.count({ where })
])
return {
meta: {
next_cursor: rows.length === limit ? rows[rows.length - 1].hashid : undefined
},
count,
groups: rows
}
}
},
instance: {
deviceCount: async function () {
return await M.Device.count({ where: { DeviceGroupId: this.id, ApplicationId: this.ApplicationId } })
},
getDevices: async function () {
return await M.Device.findAll({
where: {
DeviceGroupId: this.id,
ApplicationId: this.ApplicationId
}
})
}
}
}
}
}
1 change: 1 addition & 0 deletions forge/db/views/DeviceGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ module.exports = function (app) {
application: item.Application ? app.db.views.Application.applicationSummary(item.Application) : null,
deviceCount: item.deviceCount || 0,
devices: item.Devices ? item.Devices.map(app.db.views.Device.device) : [],
settings: item.settings,
targetSnapshot: app.db.views.ProjectSnapshot.snapshotSummary(item.targetSnapshot)
}
return filtered
Expand Down
Loading

0 comments on commit d1e8ec3

Please sign in to comment.