aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/runners/jobs.ts29
-rw-r--r--server/helpers/custom-validators/runners/jobs.ts16
-rw-r--r--server/lib/runners/runner.ts16
-rw-r--r--server/middlewares/validators/runners/jobs.ts30
-rw-r--r--server/models/runner/runner-job.ts26
-rw-r--r--server/tests/api/check-params/runners.ts52
-rw-r--r--server/tests/api/runners/runner-common.ts51
7 files changed, 192 insertions, 28 deletions
diff --git a/server/controllers/api/runners/jobs.ts b/server/controllers/api/runners/jobs.ts
index be5911b53..e9e2ddf49 100644
--- a/server/controllers/api/runners/jobs.ts
+++ b/server/controllers/api/runners/jobs.ts
@@ -5,7 +5,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger'
5import { generateRunnerJobToken } from '@server/helpers/token-generator' 5import { generateRunnerJobToken } from '@server/helpers/token-generator'
6import { MIMETYPES } from '@server/initializers/constants' 6import { MIMETYPES } from '@server/initializers/constants'
7import { sequelizeTypescript } from '@server/initializers/database' 7import { sequelizeTypescript } from '@server/initializers/database'
8import { getRunnerJobHandlerClass, updateLastRunnerContact } from '@server/lib/runners' 8import { getRunnerJobHandlerClass, runnerJobCanBeCancelled, updateLastRunnerContact } from '@server/lib/runners'
9import { 9import {
10 apiRateLimiter, 10 apiRateLimiter,
11 asyncMiddleware, 11 asyncMiddleware,
@@ -23,6 +23,7 @@ import {
23 errorRunnerJobValidator, 23 errorRunnerJobValidator,
24 getRunnerFromTokenValidator, 24 getRunnerFromTokenValidator,
25 jobOfRunnerGetValidatorFactory, 25 jobOfRunnerGetValidatorFactory,
26 listRunnerJobsValidator,
26 runnerJobGetValidator, 27 runnerJobGetValidator,
27 successRunnerJobValidator, 28 successRunnerJobValidator,
28 updateRunnerJobValidator 29 updateRunnerJobValidator
@@ -131,9 +132,17 @@ runnerJobsRouter.get('/jobs',
131 runnerJobsSortValidator, 132 runnerJobsSortValidator,
132 setDefaultSort, 133 setDefaultSort,
133 setDefaultPagination, 134 setDefaultPagination,
135 listRunnerJobsValidator,
134 asyncMiddleware(listRunnerJobs) 136 asyncMiddleware(listRunnerJobs)
135) 137)
136 138
139runnerJobsRouter.delete('/jobs/:jobUUID',
140 authenticate,
141 ensureUserHasRight(UserRight.MANAGE_RUNNERS),
142 asyncMiddleware(runnerJobGetValidator),
143 asyncMiddleware(deleteRunnerJob)
144)
145
137// --------------------------------------------------------------------------- 146// ---------------------------------------------------------------------------
138 147
139export { 148export {
@@ -374,6 +383,21 @@ async function cancelRunnerJob (req: express.Request, res: express.Response) {
374 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 383 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
375} 384}
376 385
386async function deleteRunnerJob (req: express.Request, res: express.Response) {
387 const runnerJob = res.locals.runnerJob
388
389 logger.info('Deleting job %s (%s)', runnerJob.uuid, runnerJob.type, lTags(runnerJob.uuid, runnerJob.type))
390
391 if (runnerJobCanBeCancelled(runnerJob)) {
392 const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
393 await new RunnerJobHandler().cancel({ runnerJob })
394 }
395
396 await runnerJob.destroy()
397
398 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
399}
400
377async function listRunnerJobs (req: express.Request, res: express.Response) { 401async function listRunnerJobs (req: express.Request, res: express.Response) {
378 const query: ListRunnerJobsQuery = req.query 402 const query: ListRunnerJobsQuery = req.query
379 403
@@ -381,7 +405,8 @@ async function listRunnerJobs (req: express.Request, res: express.Response) {
381 start: query.start, 405 start: query.start,
382 count: query.count, 406 count: query.count,
383 sort: query.sort, 407 sort: query.sort,
384 search: query.search 408 search: query.search,
409 stateOneOf: query.stateOneOf
385 }) 410 })
386 411
387 return res.json({ 412 return res.json({
diff --git a/server/helpers/custom-validators/runners/jobs.ts b/server/helpers/custom-validators/runners/jobs.ts
index 725a7658f..6349e79ba 100644
--- a/server/helpers/custom-validators/runners/jobs.ts
+++ b/server/helpers/custom-validators/runners/jobs.ts
@@ -1,6 +1,6 @@
1import { UploadFilesForCheck } from 'express' 1import { UploadFilesForCheck } from 'express'
2import validator from 'validator' 2import validator from 'validator'
3import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' 3import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants'
4import { 4import {
5 LiveRTMPHLSTranscodingSuccess, 5 LiveRTMPHLSTranscodingSuccess,
6 RunnerJobSuccessPayload, 6 RunnerJobSuccessPayload,
@@ -11,7 +11,7 @@ import {
11 VODHLSTranscodingSuccess, 11 VODHLSTranscodingSuccess,
12 VODWebVideoTranscodingSuccess 12 VODWebVideoTranscodingSuccess
13} from '@shared/models' 13} from '@shared/models'
14import { exists, isFileValid, isSafeFilename } from '../misc' 14import { exists, isArray, isFileValid, isSafeFilename } from '../misc'
15 15
16const RUNNER_JOBS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.RUNNER_JOBS 16const RUNNER_JOBS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.RUNNER_JOBS
17 17
@@ -56,6 +56,14 @@ function isRunnerJobErrorMessageValid (value: string) {
56 return validator.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.ERROR_MESSAGE) 56 return validator.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.ERROR_MESSAGE)
57} 57}
58 58
59function isRunnerJobStateValid (value: any) {
60 return exists(value) && RUNNER_JOB_STATES[value] !== undefined
61}
62
63function isRunnerJobArrayOfStateValid (value: any) {
64 return isArray(value) && value.every(v => isRunnerJobStateValid(v))
65}
66
59// --------------------------------------------------------------------------- 67// ---------------------------------------------------------------------------
60 68
61export { 69export {
@@ -65,7 +73,9 @@ export {
65 isRunnerJobTokenValid, 73 isRunnerJobTokenValid,
66 isRunnerJobErrorMessageValid, 74 isRunnerJobErrorMessageValid,
67 isRunnerJobProgressValid, 75 isRunnerJobProgressValid,
68 isRunnerJobAbortReasonValid 76 isRunnerJobAbortReasonValid,
77 isRunnerJobArrayOfStateValid,
78 isRunnerJobStateValid
69} 79}
70 80
71// --------------------------------------------------------------------------- 81// ---------------------------------------------------------------------------
diff --git a/server/lib/runners/runner.ts b/server/lib/runners/runner.ts
index 921cae6f2..947fdb3f0 100644
--- a/server/lib/runners/runner.ts
+++ b/server/lib/runners/runner.ts
@@ -2,8 +2,9 @@ import express from 'express'
2import { retryTransactionWrapper } from '@server/helpers/database-utils' 2import { retryTransactionWrapper } from '@server/helpers/database-utils'
3import { logger, loggerTagsFactory } from '@server/helpers/logger' 3import { logger, loggerTagsFactory } from '@server/helpers/logger'
4import { sequelizeTypescript } from '@server/initializers/database' 4import { sequelizeTypescript } from '@server/initializers/database'
5import { MRunner } from '@server/types/models/runners' 5import { MRunner, MRunnerJob } from '@server/types/models/runners'
6import { RUNNER_JOBS } from '@server/initializers/constants' 6import { RUNNER_JOBS } from '@server/initializers/constants'
7import { RunnerJobState } from '@shared/models'
7 8
8const lTags = loggerTagsFactory('runner') 9const lTags = loggerTagsFactory('runner')
9 10
@@ -32,6 +33,17 @@ function updateLastRunnerContact (req: express.Request, runner: MRunner) {
32 .finally(() => updatingRunner.delete(runner.id)) 33 .finally(() => updatingRunner.delete(runner.id))
33} 34}
34 35
36function runnerJobCanBeCancelled (runnerJob: MRunnerJob) {
37 const allowedStates = new Set<RunnerJobState>([
38 RunnerJobState.PENDING,
39 RunnerJobState.PROCESSING,
40 RunnerJobState.WAITING_FOR_PARENT_JOB
41 ])
42
43 return allowedStates.has(runnerJob.state)
44}
45
35export { 46export {
36 updateLastRunnerContact 47 updateLastRunnerContact,
48 runnerJobCanBeCancelled
37} 49}
diff --git a/server/middlewares/validators/runners/jobs.ts b/server/middlewares/validators/runners/jobs.ts
index 384b209ba..62f9340a5 100644
--- a/server/middlewares/validators/runners/jobs.ts
+++ b/server/middlewares/validators/runners/jobs.ts
@@ -1,8 +1,9 @@
1import express from 'express' 1import express from 'express'
2import { body, param } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { isUUIDValid } from '@server/helpers/custom-validators/misc' 3import { exists, isUUIDValid } from '@server/helpers/custom-validators/misc'
4import { 4import {
5 isRunnerJobAbortReasonValid, 5 isRunnerJobAbortReasonValid,
6 isRunnerJobArrayOfStateValid,
6 isRunnerJobErrorMessageValid, 7 isRunnerJobErrorMessageValid,
7 isRunnerJobProgressValid, 8 isRunnerJobProgressValid,
8 isRunnerJobSuccessPayloadValid, 9 isRunnerJobSuccessPayloadValid,
@@ -12,7 +13,9 @@ import {
12import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners' 13import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners'
13import { cleanUpReqFiles } from '@server/helpers/express-utils' 14import { cleanUpReqFiles } from '@server/helpers/express-utils'
14import { LiveManager } from '@server/lib/live' 15import { LiveManager } from '@server/lib/live'
16import { runnerJobCanBeCancelled } from '@server/lib/runners'
15import { RunnerJobModel } from '@server/models/runner/runner-job' 17import { RunnerJobModel } from '@server/models/runner/runner-job'
18import { arrayify } from '@shared/core-utils'
16import { 19import {
17 HttpStatusCode, 20 HttpStatusCode,
18 RunnerJobLiveRTMPHLSTranscodingPrivatePayload, 21 RunnerJobLiveRTMPHLSTranscodingPrivatePayload,
@@ -119,13 +122,7 @@ export const cancelRunnerJobValidator = [
119 (req: express.Request, res: express.Response, next: express.NextFunction) => { 122 (req: express.Request, res: express.Response, next: express.NextFunction) => {
120 const runnerJob = res.locals.runnerJob 123 const runnerJob = res.locals.runnerJob
121 124
122 const allowedStates = new Set<RunnerJobState>([ 125 if (runnerJobCanBeCancelled(runnerJob) !== true) {
123 RunnerJobState.PENDING,
124 RunnerJobState.PROCESSING,
125 RunnerJobState.WAITING_FOR_PARENT_JOB
126 ])
127
128 if (allowedStates.has(runnerJob.state) !== true) {
129 return res.fail({ 126 return res.fail({
130 status: HttpStatusCode.BAD_REQUEST_400, 127 status: HttpStatusCode.BAD_REQUEST_400,
131 message: 'Cannot cancel this job that is not in "pending", "processing" or "waiting for parent job" state', 128 message: 'Cannot cancel this job that is not in "pending", "processing" or "waiting for parent job" state',
@@ -137,6 +134,21 @@ export const cancelRunnerJobValidator = [
137 } 134 }
138] 135]
139 136
137export const listRunnerJobsValidator = [
138 query('search')
139 .optional()
140 .custom(exists),
141
142 query('stateOneOf')
143 .optional()
144 .customSanitizer(arrayify)
145 .custom(isRunnerJobArrayOfStateValid),
146
147 (req: express.Request, res: express.Response, next: express.NextFunction) => {
148 return next()
149 }
150]
151
140export const runnerJobGetValidator = [ 152export const runnerJobGetValidator = [
141 param('jobUUID').custom(isUUIDValid), 153 param('jobUUID').custom(isUUIDValid),
142 154
diff --git a/server/models/runner/runner-job.ts b/server/models/runner/runner-job.ts
index add6f9a43..f2ffd6a84 100644
--- a/server/models/runner/runner-job.ts
+++ b/server/models/runner/runner-job.ts
@@ -1,4 +1,4 @@
1import { FindOptions, Op, Transaction } from 'sequelize' 1import { Op, Transaction } from 'sequelize'
2import { 2import {
3 AllowNull, 3 AllowNull,
4 BelongsTo, 4 BelongsTo,
@@ -13,7 +13,7 @@ import {
13 Table, 13 Table,
14 UpdatedAt 14 UpdatedAt
15} from 'sequelize-typescript' 15} from 'sequelize-typescript'
16import { isUUIDValid } from '@server/helpers/custom-validators/misc' 16import { isArray, isUUIDValid } from '@server/helpers/custom-validators/misc'
17import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants' 17import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants'
18import { MRunnerJob, MRunnerJobRunner, MRunnerJobRunnerParent } from '@server/types/models/runners' 18import { MRunnerJob, MRunnerJobRunner, MRunnerJobRunnerParent } from '@server/types/models/runners'
19import { RunnerJob, RunnerJobAdmin, RunnerJobPayload, RunnerJobPrivatePayload, RunnerJobState, RunnerJobType } from '@shared/models' 19import { RunnerJob, RunnerJobAdmin, RunnerJobPayload, RunnerJobPrivatePayload, RunnerJobState, RunnerJobType } from '@shared/models'
@@ -227,28 +227,38 @@ export class RunnerJobModel extends Model<Partial<AttributesOnly<RunnerJobModel>
227 count: number 227 count: number
228 sort: string 228 sort: string
229 search?: string 229 search?: string
230 stateOneOf?: RunnerJobState[]
230 }) { 231 }) {
231 const { start, count, sort, search } = options 232 const { start, count, sort, search, stateOneOf } = options
232 233
233 const query: FindOptions = { 234 const query = {
234 offset: start, 235 offset: start,
235 limit: count, 236 limit: count,
236 order: getSort(sort) 237 order: getSort(sort),
238 where: []
237 } 239 }
238 240
239 if (search) { 241 if (search) {
240 if (isUUIDValid(search)) { 242 if (isUUIDValid(search)) {
241 query.where = { uuid: search } 243 query.where.push({ uuid: search })
242 } else { 244 } else {
243 query.where = { 245 query.where.push({
244 [Op.or]: [ 246 [Op.or]: [
245 searchAttribute(search, 'type'), 247 searchAttribute(search, 'type'),
246 searchAttribute(search, '$Runner.name$') 248 searchAttribute(search, '$Runner.name$')
247 ] 249 ]
248 } 250 })
249 } 251 }
250 } 252 }
251 253
254 if (isArray(stateOneOf) && stateOneOf.length !== 0) {
255 query.where.push({
256 state: {
257 [Op.in]: stateOneOf
258 }
259 })
260 }
261
252 return Promise.all([ 262 return Promise.all([
253 RunnerJobModel.scope([ ScopeNames.WITH_RUNNER ]).count(query), 263 RunnerJobModel.scope([ ScopeNames.WITH_RUNNER ]).count(query),
254 RunnerJobModel.scope([ ScopeNames.WITH_RUNNER, ScopeNames.WITH_PARENT ]).findAll<MRunnerJobRunnerParent>(query) 264 RunnerJobModel.scope([ ScopeNames.WITH_RUNNER, ScopeNames.WITH_PARENT ]).findAll<MRunnerJobRunnerParent>(query)
diff --git a/server/tests/api/check-params/runners.ts b/server/tests/api/check-params/runners.ts
index 9112ff716..7f9a0cd32 100644
--- a/server/tests/api/check-params/runners.ts
+++ b/server/tests/api/check-params/runners.ts
@@ -1,14 +1,14 @@
1import { basename } from 'path'
2/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2import { basename } from 'path'
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared' 3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
4import { 4import {
5 HttpStatusCode, 5 HttpStatusCode,
6 isVideoStudioTaskIntro, 6 isVideoStudioTaskIntro,
7 RunnerJob, 7 RunnerJob,
8 RunnerJobState, 8 RunnerJobState,
9 RunnerJobStudioTranscodingPayload,
9 RunnerJobSuccessPayload, 10 RunnerJobSuccessPayload,
10 RunnerJobUpdatePayload, 11 RunnerJobUpdatePayload,
11 RunnerJobStudioTranscodingPayload,
12 VideoPrivacy, 12 VideoPrivacy,
13 VideoStudioTaskIntro 13 VideoStudioTaskIntro
14} from '@shared/models' 14} from '@shared/models'
@@ -236,6 +236,10 @@ describe('Test managing runners', function () {
236 await checkBadSortPagination(server.url, path, server.accessToken) 236 await checkBadSortPagination(server.url, path, server.accessToken)
237 }) 237 })
238 238
239 it('Should fail with an invalid state', async function () {
240 await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
241 })
242
239 it('Should succeed to list with the correct params', async function () { 243 it('Should succeed to list with the correct params', async function () {
240 await server.runners.list({ start: 0, count: 5, sort: '-createdAt' }) 244 await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
241 }) 245 })
@@ -307,8 +311,48 @@ describe('Test managing runners', function () {
307 await checkBadSortPagination(server.url, path, server.accessToken) 311 await checkBadSortPagination(server.url, path, server.accessToken)
308 }) 312 })
309 313
310 it('Should succeed to list with the correct params', async function () { 314 it('Should fail with an invalid state', async function () {
311 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt' }) 315 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: 42 as any })
316 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ 42 ] as any })
317 })
318
319 it('Should succeed with the correct params', async function () {
320 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ RunnerJobState.COMPLETED ] })
321 })
322 })
323
324 describe('Delete', function () {
325 let jobUUID: string
326
327 before(async function () {
328 this.timeout(60000)
329
330 await server.videos.quickUpload({ name: 'video' })
331 await waitJobs([ server ])
332
333 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
334 jobUUID = availableJobs[0].uuid
335 })
336
337 it('Should fail without oauth token', async function () {
338 await server.runnerJobs.deleteByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
339 })
340
341 it('Should fail without admin rights', async function () {
342 await server.runnerJobs.deleteByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
343 })
344
345 it('Should fail with a bad job uuid', async function () {
346 await server.runnerJobs.deleteByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
347 })
348
349 it('Should fail with an unknown job uuid', async function () {
350 const jobUUID = badUUID
351 await server.runnerJobs.deleteByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
352 })
353
354 it('Should succeed with the correct params', async function () {
355 await server.runnerJobs.deleteByAdmin({ jobUUID })
312 }) 356 })
313 }) 357 })
314 358
diff --git a/server/tests/api/runners/runner-common.ts b/server/tests/api/runners/runner-common.ts
index 34a51abe7..7fed75f40 100644
--- a/server/tests/api/runners/runner-common.ts
+++ b/server/tests/api/runners/runner-common.ts
@@ -339,6 +339,30 @@ describe('Test runner common actions', function () {
339 339
340 expect(data).to.not.have.lengthOf(0) 340 expect(data).to.not.have.lengthOf(0)
341 expect(total).to.not.equal(0) 341 expect(total).to.not.equal(0)
342
343 for (const job of data) {
344 expect(job.type).to.include('hls')
345 }
346 }
347 })
348
349 it('Should filter jobs', async function () {
350 {
351 const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.WAITING_FOR_PARENT_JOB ] })
352
353 expect(data).to.not.have.lengthOf(0)
354 expect(total).to.not.equal(0)
355
356 for (const job of data) {
357 expect(job.state.label).to.equal('Waiting for parent job to finish')
358 }
359 }
360
361 {
362 const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.COMPLETED ] })
363
364 expect(data).to.have.lengthOf(0)
365 expect(total).to.equal(0)
342 } 366 }
343 }) 367 })
344 }) 368 })
@@ -598,6 +622,33 @@ describe('Test runner common actions', function () {
598 }) 622 })
599 }) 623 })
600 624
625 describe('Remove', function () {
626
627 it('Should remove a pending job', async function () {
628 await server.videos.quickUpload({ name: 'video' })
629 await waitJobs([ server ])
630
631 {
632 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
633
634 const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
635 jobUUID = pendingJob.uuid
636
637 await server.runnerJobs.deleteByAdmin({ jobUUID })
638 }
639
640 {
641 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
642
643 const parent = data.find(j => j.uuid === jobUUID)
644 expect(parent).to.not.exist
645
646 const children = data.filter(j => j.parent?.uuid === jobUUID)
647 expect(children).to.have.lengthOf(0)
648 }
649 })
650 })
651
601 describe('Stalled jobs', function () { 652 describe('Stalled jobs', function () {
602 653
603 it('Should abort stalled jobs', async function () { 654 it('Should abort stalled jobs', async function () {