@flowfuse/flowfuse
Version:
An open source low-code development platform
664 lines (583 loc) • 33.9 kB
JavaScript
const { ControllerError } = require('../../../lib/errors')
const { createSnapshot, copySnapshot, generateDeploySnapshotDescription, generateDeploySnapshotName } = require('../../../services/snapshots')
class PipelineControllerError extends ControllerError {
constructor (...args) {
super(...args)
this.name = 'PipelineControllerError'
}
}
module.exports = {
deletePipelineStage: async function (app, pipeline, stageId) {
/** @type {import('../../../lib/pipelineValidation').validateStages} */
const validateStages = app.db.models.PipelineStage.validateStages
const stage = await app.db.models.PipelineStage.byId(stageId)
if (!stage) {
throw new PipelineControllerError('not_found', 'Pipeline stage not found', 404)
}
if (stage.PipelineId !== pipeline.id) {
throw new PipelineControllerError('not_found', 'Pipeline stage not found', 404)
}
// NOTE: validation is mostly taken care of in the model layer (see models PipelineStage validate methods)
// however since we are destroying a stage (not adding/modifying), we need to explicitly validate the pipeline this time
const transaction = await app.db.sequelize.transaction()
try {
// Check if the stage is the first stage in the pipeline
const stages = await pipeline.stages()
const orderedStages = app.db.models.PipelineStage.sortStages(stages)
// Update the previous stage to point to the next stage when this model is deleted
// e.g. A -> B -> C to A -> C when B is deleted
const previousStage = orderedStages.find(s => s.NextStageId === stage.id)
// remap nextid to the next stage id
if (previousStage) {
previousStage.NextStageId = stage.NextStageId ?? null
}
const orderedStagesProposed = orderedStages.filter(s => s.id !== stage.id)
if (orderedStagesProposed.length > 0) {
validateStages(orderedStagesProposed) // will throw if invalid
}
if (previousStage) {
await previousStage.save({ transaction })
}
await stage.destroy({ transaction })
await transaction.commit()
} catch (err) {
// Rollback transaction if it exists
await transaction.rollback()
throw new PipelineControllerError('invalid_input', err.message, err.statusCode || 400, { cause: err })
}
},
/**
* Update a pipeline stage
* @param {*} app The application instance
* @param {*} pipeline A pipeline object
* @param {String|Number} stageId The ID of the stage to update
* @param {Object} options Options to update the stage with
* @param {String} [options.name] The name of the stage
* @param {String} [options.action] The action to take when deploying to this stage
* @param {String} [options.instanceId] The ID of the instance to deploy to
* @param {String} [options.deviceId] The ID of the device to deploy to
* @param {String} [options.deviceGroupId] The ID of the device group to deploy to
* @param {Boolean} [options.deployToDevices] Whether to deploy to devices of the source stage
*/
updatePipelineStage: async function (app, pipeline, stageId, options) {
const stage = await app.db.models.PipelineStage.byId(stageId)
if (!stage) {
throw new PipelineControllerError('not_found', 'Pipeline stage not found', 404)
}
if (stage.PipelineId !== pipeline.id) {
throw new PipelineControllerError('not_found', 'Pipeline stage not found', 404)
}
if (options.name) {
stage.name = options.name
}
if (options.action) {
stage.action = options.action
}
// Null will remove devices and instances, undefined skips
if (options.instanceId !== undefined || options.deviceId !== undefined || options.deviceGroupId !== undefined) {
// Check that only one of instanceId, deviceId or deviceGroupId is set
const idCount = [options.instanceId, options.deviceId, options.deviceGroupId, options.gitTokenId].filter(id => !!id).length
if (idCount > 1) {
throw new PipelineControllerError('invalid_input', 'Must provide only one instance, device, device group or git token', 400)
}
const stages = await pipeline.stages()
// stages are a linked list, so ensure we use the sorted stages
const orderedStages = app.db.models.PipelineStage.sortStages(stages)
const firstStage = orderedStages[0]
const priorStages = []
const laterStages = []
let foundStage = false
for (let stageIndex = 0; stageIndex < orderedStages.length; stageIndex++) {
const s = orderedStages[stageIndex]
if (s.id === stage.id) {
foundStage = true
continue
}
if (foundStage) {
laterStages.push(s)
} else {
priorStages.push(s)
}
}
// If this stage is being set as a device group, check all stages.
// * A device group cannot be the first stage
// * There can be multiple device groups but only a device group can follow a device group
if (options.deviceGroupId) {
if (orderedStages.length === 0) {
// this should never be reached but here for completeness
throw new PipelineControllerError('invalid_input', 'A Device Group cannot be the first stage', 400)
}
// if the first stage is the same as the stage being updated, then it's the first stage
if (firstStage && firstStage.id === stage.id) {
throw new PipelineControllerError('invalid_input', 'A Device Group cannot be the first stage', 400)
}
if (laterStages && laterStages.length) {
const nonDeviceGroupStages = laterStages.filter(s => (s.DeviceGroups?.length ?? 0) === 0)
if (nonDeviceGroupStages.length > 0) {
throw new PipelineControllerError('invalid_input', 'This stage cannot be a Device Group as a later stage contains an instance', 400)
}
}
} else if (options.gitTokenId) {
// TODO: code duplication between here and the create path to validate ownership of the gitToken
const gitTokenId = app.db.models.GitToken.decodeHashid(options.gitTokenId)
let gitToken
if (gitTokenId && gitTokenId.length === 1) {
// Verify the git token exists in the same team as this pipeline
gitToken = await app.db.models.GitToken.findOne({
where: { id: gitTokenId },
include: [
{
model: app.db.models.Team,
include: [{
model: app.db.models.Application,
where: { id: pipeline.ApplicationId }
}],
// Set required to true to ensure we match both token and application ids
required: true
}
]
})
}
if (!gitToken) {
throw new PipelineControllerError('invalid_input', 'Invalid git token')
}
} else {
// hosted/remote instance
// If a device group is set before this stage, that is an error
const deviceGroupStagesPrior = priorStages.filter(s => (s.DeviceGroups?.length ?? 0) > 0)
if (deviceGroupStagesPrior.length > 0) {
throw new PipelineControllerError('invalid_input', 'This stage cannot contain an instance as a Device Group is set in a prior stage', 400)
}
}
// Currently only one instance, device or device group per stage is supported
const instances = await stage.getInstances()
for (const instance of instances) {
await stage.removeInstance(instance)
}
const devices = await stage.getDevices()
for (const device of devices) {
await stage.removeDevice(device)
}
const deviceGroups = await stage.getDeviceGroups()
for (const deviceGroup of deviceGroups) {
await stage.removeDeviceGroup(deviceGroup)
}
const existingGitRepo = await stage.getPipelineStageGitRepo()
if (existingGitRepo && !options.gitTokenId) {
// Only destroy if the update appears to be changing stage type
await existingGitRepo.destroy()
}
if (options.instanceId) {
await stage.addInstanceId(options.instanceId)
} else if (options.deviceId) {
await stage.addDeviceId(options.deviceId)
} else if (options.deviceGroupId) {
await stage.addDeviceGroupId(options.deviceGroupId)
} else if (options.gitTokenId) {
await stage.addGitRepo(options)
}
}
if (options.deployToDevices !== undefined) {
stage.deployToDevices = options.deployToDevices
}
await stage.save()
await stage.reload()
return stage
},
addPipelineStage: async function (app, pipeline, options) {
const idCount = [options.instanceId, options.deviceId, options.deviceGroupId, options.gitTokenId].filter(id => !!id).length
if (idCount > 1) {
throw new PipelineControllerError('invalid_input', 'Cannot add a pipeline stage with a mixture of instance, device, device group or git token. Only one is permitted', 400)
} else if (idCount === 0) {
throw new PipelineControllerError('invalid_input', 'An instance, device, device group or git token is required when creating a new pipeline stage', 400)
}
if (options.instanceId) {
const instance = await app.db.models.Project.findOne({
where: { id: options.instanceId, ApplicationId: pipeline.ApplicationId },
attributes: ['id']
})
if (!instance) {
throw new PipelineControllerError('invalid_input', 'Invalid instance')
}
}
if (options.deviceId) {
const deviceId = (typeof options.deviceId === 'string') ? app.db.models.Device.decodeHashid(options.deviceId) : options.deviceId
const device = await app.db.models.Device.findOne({
where: { id: deviceId, ApplicationId: pipeline.ApplicationId },
attributes: ['id']
})
if (!device) {
throw new PipelineControllerError('invalid_input', 'Invalid device')
}
}
if (options.deviceGroupId) {
const deviceGroupId = (typeof options.deviceGroupId === 'string') ? app.db.models.DeviceGroup.decodeHashid(options.deviceGroupId) : options.deviceGroupId
const deviceGroup = await app.db.models.DeviceGroup.findOne({
where: { id: deviceGroupId, ApplicationId: pipeline.ApplicationId },
attributes: ['id']
})
if (!deviceGroup) {
throw new PipelineControllerError('invalid_input', 'Invalid device group')
}
}
if (options.gitTokenId) {
// TODO: code duplication between here and the create path to validate ownership of the gitToken
const gitTokenId = app.db.models.GitToken.decodeHashid(options.gitTokenId)
let gitToken
if (gitTokenId && gitTokenId.length === 1) {
// Verify the git token exists in the same team as this pipeline
gitToken = await app.db.models.GitToken.findOne({
where: { id: gitTokenId },
include: [
{
model: app.db.models.Team,
include: [{
model: app.db.models.Application,
where: { id: pipeline.ApplicationId }
}],
// Set required to true to ensure we match both token and application ids
required: true
}
]
})
}
if (!gitToken) {
throw new PipelineControllerError('invalid_input', 'Invalid git token')
}
}
let source
options.PipelineId = pipeline.id
if (options.source) {
// this gives us the input stage to this new stage.
// we store "targets", so need to update the source to point to this new stage
source = options.source
delete options.source
}
// Before we create the stage, we need to check a few things]
// 1. When adding a device group
// * A device group cannot be the first stage
// * There can be multiple device groups but only a device group can follow a device group
// 2. When adding an instance/device
// * A device group cannot be set before an instance or device
// 3. When adding a git repo
// * Cannot be first stage
const stages = await pipeline.stages() // stages are a linked list
const orderedStages = app.db.models.PipelineStage.sortStages(stages) // sort the stages
// 1. When adding a device group
if (options.deviceGroupId) {
const stageCount = stages.length
if (stageCount === 0) {
throw new PipelineControllerError('invalid_input', 'A Device Group cannot be the first stage', 400)
}
}
// 2. When adding an instance/device
if (options.instanceId || options.deviceId) {
// ensure that we are not adding an instance or device after a device group
const deviceGroups = orderedStages.filter(s => s.DeviceGroups?.length)
if (deviceGroups.length) {
throw new PipelineControllerError('invalid_input', 'An instance or device cannot be added after a device group', 400)
}
}
const transaction = await app.db.sequelize.transaction()
try {
const stage = await app.db.models.PipelineStage.create(options, { transaction })
if (options.instanceId) {
await stage.addInstanceId(options.instanceId, { transaction })
} else if (options.deviceId) {
await stage.addDeviceId(options.deviceId, { transaction })
} else if (options.deviceGroupId) {
await stage.addDeviceGroupId(options.deviceGroupId, { transaction })
} else if (options.gitTokenId) {
await stage.addGitRepo(options, { transaction })
} else {
// This should never be reached due to guard at top of function
throw new PipelineControllerError('invalid_input', 'Must provide an instanceId, deviceId or deviceGroupId', 400)
}
if (source) {
const sourceStage = await app.db.models.PipelineStage.byId(source, { transaction })
sourceStage.NextStageId = stage.id
await sourceStage.save({ transaction })
}
await transaction.commit()
return stage
} catch (err) {
await transaction.rollback()
throw err
}
},
validateSourceStageForDeploy: async function (app, pipeline, sourceStage) {
if (!sourceStage) {
throw new PipelineControllerError('not_found', 'Source stage not found', 404)
}
if (sourceStage.PipelineId !== pipeline.id) {
throw new PipelineControllerError('invalid_stage', 'Source stage must be part of the same pipeline', 400)
}
const targetStage = await app.db.models.PipelineStage.byId(sourceStage.NextStageId)
if (!targetStage) {
throw new PipelineControllerError('not_found', 'Target stage not found', 404)
}
if (targetStage.PipelineId !== sourceStage.PipelineId) {
throw new PipelineControllerError('invalid_stage', 'Target stage must be part of the same pipeline as source stage', 400)
}
const sourceInstances = await sourceStage.getInstances()
const sourceDevices = await sourceStage.getDevices()
const sourceDeviceGroups = await sourceStage.getDeviceGroups()
const sourceGitRepo = await sourceStage.getPipelineStageGitRepo()
const totalSources = sourceInstances.length + sourceDevices.length + sourceDeviceGroups.length + (sourceGitRepo ? 1 : 0)
if (totalSources === 0) {
throw new PipelineControllerError('invalid_stage', 'Source stage must have at least one instance, device, device group or git repo', 400)
}
if (totalSources > 1) {
throw new PipelineControllerError('invalid_stage', 'Deployments are currently only supported for source stages with a single instance or device', 400)
}
const targetInstances = await targetStage.getInstances()
const targetDevices = await targetStage.getDevices()
const targetDeviceGroups = await targetStage.getDeviceGroups()
const targetGitRepo = await targetStage.getPipelineStageGitRepo()
const totalTargets = targetInstances.length + targetDevices.length + targetDeviceGroups.length + (targetGitRepo ? 1 : 0)
if (totalTargets === 0) {
throw new PipelineControllerError('invalid_stage', 'Target stage must have at least one instance, device, device group or git repository', 400)
} else if (targetInstances.length > 1) {
throw new PipelineControllerError('invalid_stage', 'Deployments are currently only supported for target stages with a single instance, device or device group', 400)
}
const sourceInstance = sourceInstances[0]
const targetInstance = targetInstances[0]
const sourceDevice = sourceDevices[0]
const targetDevice = targetDevices[0]
const sourceDeviceGroup = sourceDeviceGroups[0]
const targetDeviceGroup = targetDeviceGroups[0]
const sourceObject = sourceInstance || sourceDevice || sourceDeviceGroup
const targetObject = targetInstance || targetDevice || targetDeviceGroup
const sourceType = sourceInstance ? 'instance' : (sourceDevice ? 'device' : (sourceDeviceGroup ? 'device group' : ''))
let sourceApplication
// Anything but a git repo must be associated with an application
if (sourceObject) {
sourceApplication = await app.db.models.Application.byId(sourceObject.ApplicationId)
if (!sourceApplication) {
throw new PipelineControllerError('invalid_stage', `Source ${sourceType} must be associated with an application`, 400)
}
}
if (sourceObject && targetObject) {
// Only applies for instance/device/device group targets
const targetType = targetInstance ? 'instance' : (targetDevice ? 'device' : (targetDeviceGroup ? 'device group' : ''))
const targetApplication = await app.db.models.Application.byId(targetObject.ApplicationId)
if (!targetApplication) {
throw new PipelineControllerError('invalid_stage', `Target ${targetType} must be associated with an application`, 400)
}
if (sourceApplication.id !== targetApplication.id || sourceApplication.TeamId !== targetApplication.TeamId) {
throw new PipelineControllerError('invalid_stage', `Source ${sourceType} and target ${targetType} must be associated with in the same team application`, 400)
}
if (targetDevice && targetDevice.mode === 'developer') {
throw new PipelineControllerError('invalid_target_stage', 'Target device cannot not be in developer mode', 400)
}
}
return {
sourceInstance,
targetInstance,
sourceDevice,
targetDevice,
sourceDeviceGroup,
targetDeviceGroup,
sourceGitRepo,
targetGitRepo,
targetStage
}
},
getOrCreateSnapshotForSourceInstance: async function (app, sourceStage, sourceInstance, sourceSnapshotId, deployMeta = { pipeline: null, user: null, targetStage: null }) {
// Only used for reporting and logging, should not be used for any logic
const { pipeline, targetStage, user } = deployMeta
if (sourceStage.action === app.db.models.PipelineStage.SNAPSHOT_ACTIONS.USE_LATEST_SNAPSHOT) {
const sourceSnapshot = await sourceInstance.getLatestSnapshot()
if (!sourceSnapshot) {
throw new PipelineControllerError('invalid_source_instance', 'No snapshots found for source stages instance but deploy action is set to use latest snapshot', 400)
}
return sourceSnapshot
}
if (sourceStage.action === app.db.models.PipelineStage.SNAPSHOT_ACTIONS.CREATE_SNAPSHOT) {
return await createSnapshot(app, sourceInstance, user, {
name: generateDeploySnapshotName(),
description: generateDeploySnapshotDescription(sourceStage, targetStage, pipeline),
setAsTarget: false // no need to deploy to devices of the source
})
}
if (sourceStage.action === app.db.models.PipelineStage.SNAPSHOT_ACTIONS.PROMPT) {
if (!sourceSnapshotId) {
throw new PipelineControllerError('invalid_source_snapshot', 'Source snapshot is required as deploy action is set to prompt for snapshot', 400)
}
const sourceSnapshot = await app.db.models.ProjectSnapshot.byId(sourceSnapshotId)
if (!sourceSnapshot) {
throw new PipelineControllerError('invalid_source_snapshot', 'Source snapshot not found', 404)
}
if (sourceSnapshot.ProjectId !== sourceInstance.id) {
throw new PipelineControllerError('invalid_source_snapshot', 'Source snapshot not associated with source instance', 400)
}
return sourceSnapshot
}
if (sourceStage.action === app.db.models.PipelineStage.SNAPSHOT_ACTIONS.USE_ACTIVE_SNAPSHOT) {
throw new PipelineControllerError('invalid_source_action', 'When using an instance as a source, use active snapshot is not supported', 400)
}
throw new PipelineControllerError('invalid_action', `Unsupported pipeline deploy action for instances: ${sourceStage.action}`, 400)
},
getOrCreateSnapshotForSourceDevice: async function (app, sourceStage, sourceDevice, sourceSnapshotId) {
if (sourceStage.action === app.db.models.PipelineStage.SNAPSHOT_ACTIONS.USE_LATEST_SNAPSHOT) {
const sourceSnapshot = await sourceDevice.getLatestSnapshot()
if (!sourceSnapshot) {
throw new PipelineControllerError('invalid_source_device', 'No snapshots found for source stages device but deploy action is set to use latest snapshot', 400)
}
return sourceSnapshot
}
if (sourceStage.action === app.db.models.PipelineStage.SNAPSHOT_ACTIONS.CREATE_SNAPSHOT) {
throw new PipelineControllerError('invalid_source_action', 'When using a device as a source, create snapshot is not supported', 400)
}
if (sourceStage.action === app.db.models.PipelineStage.SNAPSHOT_ACTIONS.PROMPT) {
if (!sourceSnapshotId) {
throw new PipelineControllerError('invalid_source_snapshot', 'Source snapshot is required as deploy action is set to prompt for snapshot', 400)
}
const sourceSnapshot = await app.db.models.ProjectSnapshot.byId(sourceSnapshotId)
if (!sourceSnapshot) {
throw new PipelineControllerError('invalid_source_snapshot', 'Source snapshot not found', 404)
}
if (sourceSnapshot.DeviceId !== sourceDevice.id) {
throw new PipelineControllerError('invalid_source_snapshot', 'Source snapshot not associated with source device', 400)
}
return sourceSnapshot
}
if (sourceStage.action === app.db.models.PipelineStage.SNAPSHOT_ACTIONS.USE_ACTIVE_SNAPSHOT) {
const sourceSnapshot = await sourceDevice.getActiveSnapshot()
if (!sourceSnapshot) {
throw new PipelineControllerError('invalid_source_device', 'No active snapshot found for source stages device but deploy action is set to use active snapshot', 400)
}
return sourceSnapshot
}
throw new PipelineControllerError('invalid_action', `Unsupported pipeline deploy action for devices: ${sourceStage.action}`, 400)
},
getSnapshotForSourceDeviceGroup: async function (app, sourceDeviceGroup) {
const sourceSnapshot = await sourceDeviceGroup.getTargetSnapshot()
if (!sourceSnapshot) {
throw new PipelineControllerError('invalid_source_device_group', 'No snapshots found for source stages device group but deploy action is set to use latest snapshot', 400)
}
return sourceSnapshot
},
/**
* Deploy a snapshot to an instance
* @param {Object} app - The application instance
* @param {Object} pipeline - The pipeline
* @param {Object} sourceStage - The source stage
* @param {Object} sourceSnapshot - The source snapshot object
* @param {Object} targetInstance - The target instance object
* @param {Object} sourceInstance - The source instance object
* @param {Object} sourceDevice - The source device object
* @param {Object} user - The user performing the deploy
* @param {Object} targetStage - The target stage
* @returns {Promise<Function>} - Resolves with the deploy is complete
*/
deploySnapshotToInstance: function (app, sourceSnapshot, targetInstance, deployToDevices, deployMeta = { pipeline: undefined, sourceStage: undefined, sourceInstance: undefined, sourceDevice: undefined, targetStage: undefined, user: undefined }) {
// Only used for reporting and logging, should not be used for any logic
const { pipeline, sourceStage, sourceInstance, sourceDevice, targetStage, user } = deployMeta
const restartTargetInstance = targetInstance?.state === 'running'
// Complete heavy work async
return (async function () {
await app.db.controllers.Project.setInflightState(targetInstance, 'importing')
await app.db.controllers.Project.setInDeploy(targetInstance)
try {
const setAsTargetForDevices = deployToDevices ?? false
const targetSnapshot = await copySnapshot(app, sourceSnapshot, targetInstance, {
importSnapshot: true, // target instance should import the snapshot
setAsTarget: setAsTargetForDevices,
targetSnapshotProperties: {
name: generateDeploySnapshotName(sourceSnapshot),
description: generateDeploySnapshotDescription(sourceStage, targetStage, pipeline, sourceSnapshot)
}
})
if (restartTargetInstance) {
await targetInstance.reload({ include: [app.db.models.Team] })
await app.containers.restartFlows(targetInstance)
}
await app.auditLog.Project.project.imported(user.id, null, targetInstance, sourceInstance, sourceDevice) // technically this isn't a project event
await app.auditLog.Project.project.snapshot.imported(user.id, null, targetInstance, sourceInstance, sourceDevice, targetSnapshot)
await app.db.controllers.Project.clearInflightState(targetInstance)
} catch (err) {
await app.db.controllers.Project.clearInflightState(targetInstance)
await app.auditLog.Project.project.imported(user.id, null, targetInstance, sourceInstance, sourceDevice) // technically this isn't a project event
await app.auditLog.Project.project.snapshot.imported(user.id, err, targetInstance, sourceInstance, sourceDevice, null)
throw new PipelineControllerError('unexpected_error', `Error during deploy: ${err.toString()}`, 500, { cause: err })
}
})()
},
/**
* Deploy a snapshot to a device
* @param {Object} app - The application instance
* @param {Object} sourceSnapshot - The source snapshot object
* @param {Object} targetDevice - The target device object
* @param {Object} pipeline - The pipeline triggering the deployment
* @param {Object} user - The user performing the deploy
* @returns {Promise<Function>} - Resolves with the deploy is complete
*/
deploySnapshotToDevice: async function (app, sourceSnapshot, targetDevice, pipeline = null, deployMeta = { user: null }) {
// Only used for reporting and logging, should not be used for any logic
const { user } = deployMeta
try {
// store original value for later audit log
const originalSnapshotId = targetDevice.targetSnapshotId
// Update the targetSnapshot of the device
await targetDevice.update({ targetSnapshotId: sourceSnapshot.id })
await app.auditLog.Device.device.snapshot.targetSet(user, null, targetDevice, sourceSnapshot)
if (pipeline) {
await app.auditLog.Device.device.pipeline.deployed(user, null, targetDevice, pipeline, targetDevice.Application, sourceSnapshot)
}
const updates = new app.auditLog.formatters.UpdatesCollection()
updates.push('targetSnapshotId', originalSnapshotId, targetDevice.targetSnapshotId)
await app.auditLog.Team.team.device.updated(user, null, targetDevice.Team, targetDevice, updates)
const updatedDevice = await app.db.models.Device.byId(targetDevice.id) // fully reload with associations
await app.db.controllers.Device.sendDeviceUpdateCommand(updatedDevice)
} catch (err) {
throw new PipelineControllerError('unexpected_error', `Error during deploy: ${err.toString()}`, 500, { cause: err })
}
},
/**
* Deploy a snapshot to a device group
* @param {Object} app - The application instance
* @param {Object} sourceSnapshot - The source snapshot object
* @param {Object} targetDeviceGroup - The target device group object
* @param {Object} user - The user performing the deploy
* @returns {Promise<Function>} - Resolves with the deploy is complete
*/
deploySnapshotToDeviceGroup: async function (app, sourceSnapshot, targetDeviceGroup, deployMeta = { user: null }) {
// Only used for reporting and logging, should not be used for any logic
// const { user } = deployMeta // TODO: implement device audit logs
try {
// store original value for later audit log
// const originalSnapshotId = targetDeviceGroup.targetSnapshotId // TODO: implement device audit logs
// Get the devices for the group
const devices = await targetDeviceGroup.getDevices()
// Get a list of the ids
const deviceIds = devices.length ? devices.map(d => d.id) : []
// start a transaction
const transaction = await app.db.sequelize.transaction()
try {
// Update the targetSnapshot of the device group
await targetDeviceGroup.PipelineStageDeviceGroup.update({ targetSnapshotId: sourceSnapshot.id }, { transaction })
// Update the targetSnapshotId on the device group
await targetDeviceGroup.update({ targetSnapshotId: sourceSnapshot.id }, { transaction })
if (deviceIds.length > 0) {
// update all devices targetSnapshotId
await app.db.models.Device.update({ targetSnapshotId: sourceSnapshot.id }, { where: { id: deviceIds }, transaction })
}
// commit the transaction
await transaction.commit()
} catch (error) {
// rollback the transaction
await transaction.rollback()
throw error
}
// TODO: implement device audit logs
// const updates = new app.auditLog.formatters.UpdatesCollection()
// updates.push('targetSnapshotId', originalSnapshotId, targetDeviceGroup.targetSnapshotId)
// await app.auditLog.Team.team.device.updated(user, null, targetDeviceGroup.Team, targetDeviceGroup, updates)
// Send the update command asynchronously
app.db.controllers.DeviceGroup.sendUpdateCommand(targetDeviceGroup)
} catch (err) {
throw new PipelineControllerError('unexpected_error', `Error during deploy: ${err.toString()}`, 500, { cause: err })
}
}
}