@flowfuse/flowfuse
Version:
An open source low-code development platform
174 lines (165 loc) • 7.59 kB
JavaScript
/*
`frontend/src/utils/pipelineValidation.js` and `forge/lib/pipelineValidation.js` are 1:1 copies
of each other and should be kept in sync to maintain domain cohesion and separation of concerns.
*/
// NOTE: The below PipelineStage Type definitions are not exact types but used in this code file only
// for clarity of the props and types we use in these routines.
/**
* @typedef {Object} PipelineStageServerSide
* @property {Array<Object>} [Instances] - Array of instances in the stage (if it contains any, then this stage is an INSTANCE)
* @property {Array<Object>} [Devices] - Array of devices in the stage (if it contains any, then this stage is a DEVICE)
* @property {Array<Object>} [DeviceGroups] - Array of device groups in the stage (if it contains any, then this stage is a DEVICE_GROUP)
* @property {Object} [PipelineStageGitRepo] - The git repository for the stage (if this is an object then this stage is a GITHUB_REPO)
*/
/**
* @typedef {Object} PipelineStageClientSide
* @property {Object} [instance] - The instance associated with the stage (if present, this stage is an INSTANCE)
* @property {Object} [device] - The device associated with the stage (if present, this stage is a DEVICE)
* @property {Object} [deviceGroup] - The deviceGroup associated with the stage (if present, this stage is an DEVICE_GROUP)
* @property {Object} [gitRepo] - The "view" compatible alternative for PipelineStageGitRepo object that signifies this stage is a GITHUB_REPO
* @property {'instance'|'device'|'device-group'|'git-repo'} [stageType] - The type of the stage (e.g., 'device', 'instance', 'device-group', 'git-repo')
*/
/**
* @typedef {PipelineStageServerSide & PipelineStageClientSide} PipelineStage
* @property {string} [id] - The ID of the stage
* @property {string} [name] - The name of the stage
*/
const pipelineStageRules = {
INSTANCE: {
type: 'INSTANCE',
description: 'Hosted Instance',
canBeFirst: true,
canBeLast: true,
canPrecede: ['*'],
canFollow: ['INSTANCE', 'DEVICE', 'GITHUB_REPO'],
mustBeLast: false,
canBeMoreThanOne: true
},
DEVICE: {
type: 'DEVICE',
description: 'Remote Instance',
canBeFirst: true,
canBeLast: true,
canPrecede: ['*'],
canFollow: ['INSTANCE', 'DEVICE', 'GITHUB_REPO'],
mustBeLast: false,
canBeMoreThanOne: true
},
DEVICE_GROUP: {
type: 'DEVICE_GROUP',
description: 'Device Group',
canBeFirst: false,
canBeLast: true,
canPrecede: ['DEVICE_GROUP', 'GITHUB_REPO'],
canFollow: ['DEVICE_GROUP', 'INSTANCE', 'DEVICE', 'GITHUB_REPO'],
canBeMoreThanOne: true,
mustBeLast: false
},
GITHUB_REPO: {
type: 'GITHUB_REPO',
description: 'Git Repository',
canBeFirst: true,
canBeLast: true,
canFollow: ['*'],
canPrecede: ['*'],
mustBeLast: false,
canBeMoreThanOne: true
}
}
// freeze the rules so they cannot be modified
Object.freeze(pipelineStageRules)
/**
* Get the pipeline stage rule for a given stage
* @param {PipelineStage} stage - The pipeline stage to get the rule for
* @returns {Object|null} The pipeline stage rule for the given stage, or null if no rule is found
*/
const getPipelineStageRule = (stage) => {
if (!stage) {
return null
}
if (stage.DeviceGroups?.length || stage.deviceGroup || stage.stageType === 'device-group') {
return pipelineStageRules.DEVICE_GROUP
} else if (stage.Devices?.length || stage.device || stage.stageType === 'device') {
return pipelineStageRules.DEVICE
} else if (stage.Instances?.length || stage.instance || stage.stageType === 'instance') {
return pipelineStageRules.INSTANCE
} else if (stage.PipelineStageGitRepo?.GitTokenId || stage.gitRepo?.GitTokenId || stage.stageType === 'git-repo') {
return pipelineStageRules.GITHUB_REPO
}
return null
}
/**
* Get the pipeline rules for a given ordered list of stages
* @param {Array<PipelineStage>} orderedStages - The ordered list of pipeline stages to get the rules for
* @returns {Array<Object>} The pipeline rules for the given ordered list of stages
*/
const getPipelineRules = (orderedStages) => {
const pipelineRules = []
for (const stage of orderedStages) {
const rule = getPipelineStageRule(stage)
if (!rule) {
throw new Error('Unrecognised pipeline stage')
}
pipelineRules.push(rule)
}
return pipelineRules
}
module.exports = {
/**
* Validate the stages of a pipeline
* @param {Array<PipelineStage>} orderedStages Pipeline stages to validate (MUST be in order - use the helper function `app.db.models.PipelineStage.sortStages` to get the ordered stages)
* @returns {boolean} `true` if the stages are valid
*/
validateStages (orderedStages) {
try {
const rules = getPipelineRules(orderedStages)
let prevRule = null
const typeCounts = {
INSTANCE: 0,
DEVICE: 0,
DEVICE_GROUP: 0,
GITHUB_REPO: 0
}
for (let i = 0; i < rules.length; i++) {
const thisRule = rules[i]
const isFirst = i === 0
const isLast = i === rules.length - 1
const nextRule = rules[i + 1] || null
typeCounts[thisRule.type]++
if (typeCounts[thisRule.type] > 1 && !thisRule.canBeMoreThanOne) {
throw new Error(`A ${thisRule.description} Pipeline stage cannot appear more than once`)
}
if (isFirst && thisRule.canBeFirst === false) {
throw new Error(`A ${thisRule.description} Pipeline stage cannot be the first stage`)
}
if (isLast && thisRule.canBeLast === false) {
throw new Error(`A ${thisRule.description} Pipeline stage cannot be the last stage`)
}
if (thisRule.mustBeLast && !isLast) {
throw new Error(`A ${thisRule.description} Pipeline stage must be the last stage`)
}
if (nextRule) {
if (!thisRule.canPrecede.includes(nextRule.type) && !thisRule.canPrecede.includes('*')) {
throw new Error(`A ${thisRule.description} Pipeline stage cannot precede a ${nextRule.description}`)
}
if (!nextRule.canFollow.includes(thisRule.type) && !nextRule.canFollow.includes('*')) {
throw new Error(`A ${nextRule.description} Pipeline stage cannot follow a ${thisRule.description}`)
}
}
if (prevRule) {
if (!prevRule.canPrecede.includes(thisRule.type) && !prevRule.canPrecede.includes('*')) {
throw new Error(`A ${prevRule.description} Pipeline stage cannot precede a ${thisRule.description}`)
}
if (!thisRule.canFollow.includes(prevRule.type) && !thisRule.canFollow.includes('*')) {
throw new Error(`A ${thisRule.description} Pipeline stage cannot follow a ${prevRule.description}`)
}
}
prevRule = thisRule
}
return true
} catch (err) {
err.code = 'pipeline_validation_failed'
throw err
}
}
}