Skip to content

Commit

Permalink
Merge pull request #4847 from FlowFuse/team-pipeline-api
Browse files Browse the repository at this point in the history
Team Pipelines API
  • Loading branch information
cstns authored Dec 2, 2024
2 parents f63e144 + 5f9bc01 commit fa4f8c4
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 1 deletion.
13 changes: 13 additions & 0 deletions forge/ee/db/models/Pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ module.exports = {
ApplicationId: applicationId
}
})
},
byTeamId: async function (teamId) {
if (typeof teamId === 'string') {
teamId = M.Team.decodeHashid(teamId)
}
return self.findAll({
include: [{
association: 'Application',
where: {
TeamId: teamId
}
}]
})
}
}
}
Expand Down
28 changes: 27 additions & 1 deletion forge/ee/db/views/Pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ module.exports = function (app) {
return null
}
}
async function teamPipeline (pipeline) {
if (pipeline) {
const result = pipeline.toJSON()
// TODO: This is an N+1 query
const stages = await app.db.models.PipelineStage.byPipeline(result.id, { includeDeviceStatus: true })
const filtered = {
id: result.hashid,
name: result.name,
application: {
id: result.Application.hashid,
name: result.Application.name
},
stages: await app.db.views.PipelineStage.stageList(stages)
}

return filtered
} else {
return null
}
}

app.addSchema({
$id: 'PipelineList',
Expand All @@ -37,8 +57,14 @@ module.exports = function (app) {
return list
}

async function teamPipelineList (pipelines) {
const list = await Promise.all(pipelines.map(teamPipeline))
return list
}

return {
pipeline,
pipelineList
pipelineList,
teamPipelineList
}
}
1 change: 1 addition & 0 deletions forge/ee/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = async function (app) {
}
await app.register(require('./sharedLibrary'), { logLevel: app.config.logging.http })
await app.register(require('./pipeline'), { prefix: '/api/v1', logLevel: app.config.logging.http })
await app.register(require('./pipeline/teamPipelines.js'), { prefix: '/api/v1/teams/:teamId/pipelines', logLevel: app.config.logging.http })
await app.register(require('./deviceEditor'), { prefix: '/api/v1/devices/:deviceId/editor', logLevel: app.config.logging.http })
await app.register(require('./bom/application.js'), { prefix: '/api/v1/applications', logLevel: app.config.logging.http })

Expand Down
41 changes: 41 additions & 0 deletions forge/ee/routes/pipeline/teamPipelines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module.exports = async function (app) {
/**
* List all pipelines within an Team
* /api/v1/teams/:teamId/pipelines
*/
app.get('/', {
preHandler: app.needsPermission('team:pipeline:list'),
schema: {
summary: '',
tags: ['Pipelines'],
params: {
type: 'object',
properties: {
teamId: { type: 'string' }
}
},
response: {
200: {
type: 'object',
properties: {
count: { type: 'number' },
pipelines: { $ref: 'PipelineList' }
}
},
'4xx': {
$ref: 'APIError'
}
}
}
}, async (request, reply) => {
const pipelines = await app.db.models.Pipeline.byTeamId(request.params.teamId)
if (pipelines) {
reply.send({
count: pipelines.length,
pipelines: await app.db.views.Pipeline.teamPipelineList(pipelines)
})
} else {
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
}
})
}
1 change: 1 addition & 0 deletions forge/lib/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ const Permissions = {
'pipeline:edit': { description: 'Edit a pipeline', role: Roles.Owner },
'pipeline:delete': { description: 'Delete a pipeline', role: Roles.Owner },
'application:pipeline:list': { description: 'List pipelines within an application', role: Roles.Member },
'team:pipeline:list': { description: 'List pipelines within a team', role: Roles.Member },

// SAML
'saml-provider:create': { description: 'Create a SAML Provider', role: Roles.Admin },
Expand Down
49 changes: 49 additions & 0 deletions test/unit/forge/ee/routes/api/pipeline_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2867,6 +2867,55 @@ describe('Pipelines API', function () {
response.statusCode.should.equal(200)
})
})
describe('List Team Pipelines', function () {
it('should list all the pipelines in a team and include stages, instances, devices, device groups and applications', async function () {
const response = await app.inject({
method: 'GET',
url: `/api/v1/teams/${TestObjects.team.hashid}/pipelines`,
cookies: { sid: TestObjects.tokens.alice }
})

const body = await response.json()

body.should.have.property('count', 3)
body.pipelines.should.have.length(3)

body.pipelines[0].should.have.property('name', 'new-pipeline')
body.pipelines[0].should.have.property('stages')
body.pipelines[0].should.have.property('application')
body.pipelines[0].application.should.have.property('name', TestObjects.application.name)

body.pipelines[0].stages.should.have.length(1)
body.pipelines[0].stages[0].should.have.property('name', 'stage-one')

body.pipelines[0].stages[0].instances.should.have.length(1)
body.pipelines[0].stages[0].instances[0].should.have.property('name', 'project1')

body.pipelines[1].should.have.property('name', 'new-pipeline-devices')
body.pipelines[1].should.have.property('application')

body.pipelines[1].stages.should.have.length(1)
body.pipelines[1].stages[0].should.have.property('name', 'stage-one-devices')

body.pipelines[1].stages[0].devices.should.have.length(1)
body.pipelines[1].stages[0].devices[0].should.have.property('name', 'device-a')

body.pipelines[2].should.have.property('name', 'new-pipeline-device-groups')
body.pipelines[2].should.have.property('application')

body.pipelines[2].stages.should.have.length(2)
body.pipelines[2].stages[0].should.have.property('name', 'stage-one-instance') // first stage is an instance
body.pipelines[2].stages[1].should.have.property('name', 'stage-two-device-group') // second stage is a device group

body.pipelines[2].stages[0].instances.should.have.length(1)
body.pipelines[2].stages[0].instances[0].should.have.property('name', 'project1')

body.pipelines[2].stages[1].deviceGroups.should.have.length(1)
body.pipelines[2].stages[1].deviceGroups[0].should.have.property('name', 'device-group-a')

response.statusCode.should.equal(200)
})
})

describe('Locked template fields', function () {
async function isDeployComplete (instance) {
Expand Down

0 comments on commit fa4f8c4

Please sign in to comment.