1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-06 03:50:26 +02:00

Add runner version info

This commit is contained in:
Chocobozzz 2025-07-29 10:30:23 +02:00
parent 309068ae1d
commit 3e1cdb9fa2
No known key found for this signature in database
GPG key ID: 583A612D890159BE
23 changed files with 233 additions and 192 deletions

View file

@ -1,10 +1,10 @@
import { pick, shuffle, wait } from '@peertube/peertube-core-utils'
import { PeerTubeProblemDocument, RunnerJobType, ServerErrorCode } from '@peertube/peertube-models'
import { PeerTubeServer as PeerTubeServerCommand } from '@peertube/peertube-server-commands'
import { ensureDir, remove } from 'fs-extra/esm' import { ensureDir, remove } from 'fs-extra/esm'
import { readdir } from 'fs/promises' import { readdir } from 'fs/promises'
import { join } from 'path' import { join } from 'path'
import { io, Socket } from 'socket.io-client' import { io, Socket } from 'socket.io-client'
import { pick, shuffle, wait } from '@peertube/peertube-core-utils'
import { PeerTubeProblemDocument, RunnerJobType, ServerErrorCode } from '@peertube/peertube-models'
import { PeerTubeServer as PeerTubeServerCommand } from '@peertube/peertube-server-commands'
import { ConfigManager } from '../shared/index.js' import { ConfigManager } from '../shared/index.js'
import { IPCServer } from '../shared/ipc/index.js' import { IPCServer } from '../shared/ipc/index.js'
import { logger } from '../shared/logger.js' import { logger } from '../shared/logger.js'
@ -95,7 +95,12 @@ export class RunnerServer {
logger.info(`Registering runner ${runnerName} on ${url}...`) logger.info(`Registering runner ${runnerName} on ${url}...`)
const serverCommand = new PeerTubeServerCommand({ url }) const serverCommand = new PeerTubeServerCommand({ url })
const { runnerToken } = await serverCommand.runners.register({ name: runnerName, description: runnerDescription, registrationToken }) const { runnerToken } = await serverCommand.runners.register({
name: runnerName,
description: runnerDescription,
registrationToken,
version: process.env.PACKAGE_VERSION
})
const server: PeerTubeServer = Object.assign(serverCommand, { const server: PeerTubeServer = Object.assign(serverCommand, {
runnerToken, runnerToken,
@ -268,7 +273,9 @@ export class RunnerServer {
jobTypes: this.enabledJobsArray.length !== getSupportedJobsList().length jobTypes: this.enabledJobsArray.length !== getSupportedJobsList().length
? this.enabledJobsArray ? this.enabledJobsArray
: undefined : undefined,
version: process.env.PACKAGE_VERSION
}) })
// FIXME: remove in PeerTube v8: jobTypes has been introduced in PeerTube v7, so do the filter here too // FIXME: remove in PeerTube v8: jobTypes has been introduced in PeerTube v7, so do the filter here too

View file

@ -18,6 +18,8 @@
<td>{{ runner.ip }}</td> <td>{{ runner.ip }}</td>
<td>{{ runner.version }}</td>
<td>{{ runner.lastContact | ptDate: 'short' }}</td> <td>{{ runner.lastContact | ptDate: 'short' }}</td>
<td>{{ runner.createdAt | ptDate: 'short' }}</td> <td>{{ runner.createdAt | ptDate: 'short' }}</td>

View file

@ -32,6 +32,7 @@ export class RunnerListComponent implements OnInit {
{ id: 'name', label: $localize`Name`, sortable: false }, { id: 'name', label: $localize`Name`, sortable: false },
{ id: 'description', label: $localize`Description`, sortable: false }, { id: 'description', label: $localize`Description`, sortable: false },
{ id: 'ip', label: $localize`IP`, sortable: false }, { id: 'ip', label: $localize`IP`, sortable: false },
{ id: 'version', label: $localize`Version`, sortable: false },
{ id: 'lastContact', label: $localize`Last contact`, sortable: false }, { id: 'lastContact', label: $localize`Last contact`, sortable: false },
{ id: 'createdAt', label: $localize`Created`, sortable: true } { id: 'createdAt', label: $localize`Created`, sortable: true }
] ]

View file

@ -3,4 +3,5 @@ export interface RegisterRunnerBody {
name: string name: string
description?: string description?: string
version?: string
} }

View file

@ -3,4 +3,5 @@ import { RunnerJobType } from './runner-jobs/runner-job-type.type.js'
export interface RequestRunnerJobBody { export interface RequestRunnerJobBody {
runnerToken: string runnerToken: string
jobTypes?: RunnerJobType[] jobTypes?: RunnerJobType[]
version?: string
} }

View file

@ -7,6 +7,8 @@ export interface Runner {
ip: string ip: string
lastContact: Date | string lastContact: Date | string
version: string
createdAt: Date | string createdAt: Date | string
updatedAt: Date | string updatedAt: Date | string
} }

View file

@ -82,7 +82,7 @@ export class RunnerJobsCommand extends AbstractCommand {
...options, ...options,
path, path,
fields: pick(options, [ 'runnerToken', 'jobTypes' ]), fields: pick(options, [ 'runnerToken', 'jobTypes', 'version' ]),
implicitToken: false, implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200 defaultExpectedStatus: HttpStatusCode.OK_200
})) }))

View file

@ -12,7 +12,6 @@ import { unwrapBody } from '../requests/index.js'
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
export class RunnersCommand extends AbstractCommand { export class RunnersCommand extends AbstractCommand {
list (options: OverrideCommandOptions & { list (options: OverrideCommandOptions & {
start?: number start?: number
count?: number count?: number
@ -37,7 +36,7 @@ export class RunnersCommand extends AbstractCommand {
...options, ...options,
path, path,
fields: pick(options, [ 'name', 'registrationToken', 'description' ]), fields: pick(options, [ 'name', 'registrationToken', 'description', 'version' ]),
implicitToken: true, implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200 defaultExpectedStatus: HttpStatusCode.OK_200
})) }))
@ -56,9 +55,11 @@ export class RunnersCommand extends AbstractCommand {
}) })
} }
delete (options: OverrideCommandOptions & { delete (
options: OverrideCommandOptions & {
id: number id: number
}) { }
) {
const path = '/api/v1/runners/' + options.id const path = '/api/v1/runners/' + options.id
return this.deleteRequest({ return this.deleteRequest({

View file

@ -96,9 +96,7 @@ describe('Test managing runners', function () {
}) })
describe('Managing runner registration tokens', function () { describe('Managing runner registration tokens', function () {
describe('Common', function () { describe('Common', function () {
it('Should fail to generate, list or delete runner registration token without oauth token', async function () { it('Should fail to generate, list or delete runner registration token without oauth token', async function () {
const expectedStatus = HttpStatusCode.UNAUTHORIZED_401 const expectedStatus = HttpStatusCode.UNAUTHORIZED_401
@ -117,7 +115,6 @@ describe('Test managing runners', function () {
}) })
describe('Delete', function () { describe('Delete', function () {
it('Should fail to delete with a bad id', async function () { it('Should fail to delete with a bad id', async function () {
await server.runnerRegistrationTokens.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) await server.runnerRegistrationTokens.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
}) })
@ -175,6 +172,13 @@ describe('Test managing runners', function () {
await server.runners.register({ name, description: 'a'.repeat(5000), registrationToken, expectedStatus }) await server.runners.register({ name, description: 'a'.repeat(5000), registrationToken, expectedStatus })
}) })
it('Should fail with an invalid version', async function () {
const expectedStatus = HttpStatusCode.BAD_REQUEST_400
await server.runners.register({ name, version: '', registrationToken, expectedStatus })
await server.runners.register({ name, version: 'a'.repeat(5000), registrationToken, expectedStatus })
})
it('Should succeed with the correct params', async function () { it('Should succeed with the correct params', async function () {
const { id } = await server.runners.register({ name, description: 'super description', registrationToken }) const { id } = await server.runners.register({ name, description: 'super description', registrationToken })
@ -192,7 +196,6 @@ describe('Test managing runners', function () {
}) })
describe('Delete', function () { describe('Delete', function () {
it('Should fail without oauth token', async function () { it('Should fail without oauth token', async function () {
await server.runners.delete({ token: null, id: toDeleteId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) await server.runners.delete({ token: null, id: toDeleteId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
}) })
@ -245,11 +248,9 @@ describe('Test managing runners', function () {
await server.runners.list({ start: 0, count: 5, sort: '-createdAt' }) await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
}) })
}) })
}) })
describe('Runner jobs by admin', function () { describe('Runner jobs by admin', function () {
describe('Cancel', function () { describe('Cancel', function () {
let jobUUID: string let jobUUID: string
@ -356,7 +357,6 @@ describe('Test managing runners', function () {
await server.runnerJobs.deleteByAdmin({ jobUUID }) await server.runnerJobs.deleteByAdmin({ jobUUID })
}) })
}) })
}) })
describe('Runner jobs by runners', function () { describe('Runner jobs by runners', function () {
@ -490,7 +490,6 @@ describe('Test managing runners', function () {
}) })
describe('Common runner tokens validations', function () { describe('Common runner tokens validations', function () {
async function testEndpoints (options: { async function testEndpoints (options: {
jobUUID: string jobUUID: string
runnerToken: string runnerToken: string
@ -597,7 +596,6 @@ describe('Test managing runners', function () {
}) })
describe('Unregister', function () { describe('Unregister', function () {
it('Should fail without a runner token', async function () { it('Should fail without a runner token', async function () {
await server.runners.unregister({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) await server.runners.unregister({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
}) })
@ -612,7 +610,6 @@ describe('Test managing runners', function () {
}) })
describe('Request', function () { describe('Request', function () {
it('Should fail without a runner token', async function () { it('Should fail without a runner token', async function () {
await server.runnerJobs.request({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) await server.runnerJobs.request({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
}) })
@ -629,13 +626,16 @@ describe('Test managing runners', function () {
await server.runnerJobs.request({ runnerToken, jobTypes: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) await server.runnerJobs.request({ runnerToken, jobTypes: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
}) })
it('Should fail with an invalid runner version', async function () {
await server.runnerJobs.request({ runnerToken, jobTypes: [], version: 'invalid', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
it('Should succeed with the correct params', async function () { it('Should succeed with the correct params', async function () {
await server.runnerJobs.request({ runnerToken, jobTypes: [] }) await server.runnerJobs.request({ runnerToken, jobTypes: [] })
}) })
}) })
describe('Accept', function () { describe('Accept', function () {
it('Should fail with a bad a job uuid', async function () { it('Should fail with a bad a job uuid', async function () {
await server.runnerJobs.accept({ jobUUID: '', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) await server.runnerJobs.accept({ jobUUID: '', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
}) })
@ -663,7 +663,6 @@ describe('Test managing runners', function () {
}) })
describe('Abort', function () { describe('Abort', function () {
it('Should fail without a reason', async function () { it('Should fail without a reason', async function () {
await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
}) })
@ -685,9 +684,7 @@ describe('Test managing runners', function () {
}) })
describe('Update', function () { describe('Update', function () {
describe('Common', function () { describe('Common', function () {
it('Should fail with an invalid progress', async function () { it('Should fail with an invalid progress', async function () {
await server.runnerJobs.update({ jobUUID, jobToken, runnerToken, progress: 101, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) await server.runnerJobs.update({ jobUUID, jobToken, runnerToken, progress: 101, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
}) })
@ -742,7 +739,6 @@ describe('Test managing runners', function () {
}) })
describe('Error', function () { describe('Error', function () {
it('Should fail with a missing error message', async function () { it('Should fail with a missing error message', async function () {
await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
}) })
@ -768,7 +764,6 @@ describe('Test managing runners', function () {
let vodJobToken: string let vodJobToken: string
describe('Common', function () { describe('Common', function () {
it('Should fail with a job not in processing state', async function () { it('Should fail with a job not in processing state', async function () {
await server.runnerJobs.success({ await server.runnerJobs.success({
jobUUID: completedJobUUID, jobUUID: completedJobUUID,
@ -781,7 +776,6 @@ describe('Test managing runners', function () {
}) })
describe('VOD', function () { describe('VOD', function () {
it('Should fail with an invalid vod web video payload', async function () { it('Should fail with an invalid vod web video payload', async function () {
const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-web-video-transcoding' }) const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-web-video-transcoding' })
@ -834,7 +828,6 @@ describe('Test managing runners', function () {
}) })
describe('Video studio', function () { describe('Video studio', function () {
it('Should fail with an invalid video studio transcoding payload', async function () { it('Should fail with an invalid video studio transcoding payload', async function () {
await server.runnerJobs.success({ await server.runnerJobs.success({
jobUUID: studioAcceptedJob.uuid, jobUUID: studioAcceptedJob.uuid,
@ -848,9 +841,7 @@ describe('Test managing runners', function () {
}) })
describe('Job files', function () { describe('Job files', function () {
describe('Check video param for common job file routes', function () { describe('Check video param for common job file routes', function () {
async function fetchFiles (options: { async function fetchFiles (options: {
videoUUID?: string videoUUID?: string
expectedStatus: HttpStatusCodeType expectedStatus: HttpStatusCodeType
@ -898,7 +889,6 @@ describe('Test managing runners', function () {
}) })
describe('Video studio tasks file routes', function () { describe('Video studio tasks file routes', function () {
it('Should fail with an invalid studio filename', async function () { it('Should fail with an invalid studio filename', async function () {
await fetchStudioFiles({ await fetchStudioFiles({
videoUUID: videoStudioUUID, videoUUID: videoStudioUUID,

View file

@ -154,7 +154,8 @@ describe('Test runner common actions', function () {
await server.runners.register({ await server.runners.register({
name: 'runner 2', name: 'runner 2',
registrationToken registrationToken,
version: '1.0.0'
}) })
const { total, data } = await server.runners.list({ sort: 'createdAt' }) const { total, data } = await server.runners.list({ sort: 'createdAt' })
@ -173,9 +174,11 @@ describe('Test runner common actions', function () {
expect(data[0].name).to.equal('runner 1') expect(data[0].name).to.equal('runner 1')
expect(data[0].description).to.equal('my super runner 1') expect(data[0].description).to.equal('my super runner 1')
expect(data[0].version).to.not.exist
expect(data[1].name).to.equal('runner 2') expect(data[1].name).to.equal('runner 2')
expect(data[1].description).to.be.null expect(data[1].description).to.be.null
expect(data[1].version).to.equal('1.0.0')
toDelete = data[1] toDelete = data[1]
}) })
@ -244,7 +247,6 @@ describe('Test runner common actions', function () {
} }
describe('List jobs', function () { describe('List jobs', function () {
it('Should not have jobs', async function () { it('Should not have jobs', async function () {
const { total, data } = await server.runnerJobs.list() const { total, data } = await server.runnerJobs.list()
@ -369,7 +371,6 @@ describe('Test runner common actions', function () {
}) })
describe('Accept/update/abort/process a job', function () { describe('Accept/update/abort/process a job', function () {
it('Should request available jobs', async function () { it('Should request available jobs', async function () {
lastRunnerContact = new Date() lastRunnerContact = new Date()
@ -395,6 +396,13 @@ describe('Test runner common actions', function () {
jobUUID = webVideoJobs[0].uuid jobUUID = webVideoJobs[0].uuid
}) })
it('Should update runner version', async function () {
await server.runnerJobs.request({ runnerToken, version: '2.0.0' })
const { data } = await server.runners.list({ sort: 'createdAt' })
expect(data[0].version).to.equal('2.0.0')
})
it('Should filter requested jobs', async function () { it('Should filter requested jobs', async function () {
{ {
const { availableJobs } = await server.runnerJobs.request({ runnerToken, jobTypes: [ 'vod-web-video-transcoding' ] }) const { availableJobs } = await server.runnerJobs.request({ runnerToken, jobTypes: [ 'vod-web-video-transcoding' ] })
@ -526,7 +534,6 @@ describe('Test runner common actions', function () {
}) })
describe('Error job', function () { describe('Error job', function () {
it('Should accept another job and post an error', async function () { it('Should accept another job and post an error', async function () {
await server.runnerJobs.cancelAllJobs() await server.runnerJobs.cancelAllJobs()
await server.videos.quickUpload({ name: 'video' }) await server.videos.quickUpload({ name: 'video' })
@ -588,7 +595,6 @@ describe('Test runner common actions', function () {
}) })
describe('Cancel', function () { describe('Cancel', function () {
it('Should cancel a pending job', async function () { it('Should cancel a pending job', async function () {
await server.videos.quickUpload({ name: 'video' }) await server.videos.quickUpload({ name: 'video' })
await waitJobs([ server ]) await waitJobs([ server ])
@ -636,7 +642,6 @@ describe('Test runner common actions', function () {
}) })
describe('Remove', function () { describe('Remove', function () {
it('Should remove a pending job', async function () { it('Should remove a pending job', async function () {
await server.videos.quickUpload({ name: 'video' }) await server.videos.quickUpload({ name: 'video' })
await waitJobs([ server ]) await waitJobs([ server ])
@ -663,7 +668,6 @@ describe('Test runner common actions', function () {
}) })
describe('Stalled jobs', function () { describe('Stalled jobs', function () {
it('Should abort stalled jobs', async function () { it('Should abort stalled jobs', async function () {
this.timeout(60000) this.timeout(60000)
@ -689,7 +693,6 @@ describe('Test runner common actions', function () {
}) })
describe('Rate limit', function () { describe('Rate limit', function () {
before(async function () { before(async function () {
this.timeout(60000) this.timeout(60000)

View file

@ -46,6 +46,7 @@ import {
getRunnerFromTokenValidator, getRunnerFromTokenValidator,
jobOfRunnerGetValidatorFactory, jobOfRunnerGetValidatorFactory,
listRunnerJobsValidator, listRunnerJobsValidator,
requestRunnerJobValidator,
runnerJobGetValidator, runnerJobGetValidator,
successRunnerJobValidator, successRunnerJobValidator,
updateRunnerJobValidator updateRunnerJobValidator
@ -72,13 +73,16 @@ const runnerJobsRouter = express.Router()
// Controllers for runners // Controllers for runners
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
runnerJobsRouter.post('/jobs/request', runnerJobsRouter.post(
'/jobs/request',
apiRateLimiter, apiRateLimiter,
requestRunnerJobValidator,
asyncMiddleware(getRunnerFromTokenValidator), asyncMiddleware(getRunnerFromTokenValidator),
asyncMiddleware(requestRunnerJob) asyncMiddleware(requestRunnerJob)
) )
runnerJobsRouter.post('/jobs/:jobUUID/accept', runnerJobsRouter.post(
'/jobs/:jobUUID/accept',
apiRateLimiter, apiRateLimiter,
asyncMiddleware(runnerJobGetValidator), asyncMiddleware(runnerJobGetValidator),
acceptRunnerJobValidator, acceptRunnerJobValidator,
@ -86,14 +90,16 @@ runnerJobsRouter.post('/jobs/:jobUUID/accept',
asyncMiddleware(acceptRunnerJob) asyncMiddleware(acceptRunnerJob)
) )
runnerJobsRouter.post('/jobs/:jobUUID/abort', runnerJobsRouter.post(
'/jobs/:jobUUID/abort',
apiRateLimiter, apiRateLimiter,
asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])), asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])),
abortRunnerJobValidator, abortRunnerJobValidator,
asyncMiddleware(abortRunnerJob) asyncMiddleware(abortRunnerJob)
) )
runnerJobsRouter.post('/jobs/:jobUUID/update', runnerJobsRouter.post(
'/jobs/:jobUUID/update',
runnerJobUpdateVideoFiles, runnerJobUpdateVideoFiles,
apiRateLimiter, // Has to be after multer middleware to parse runner token apiRateLimiter, // Has to be after multer middleware to parse runner token
asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING, RunnerJobState.COMPLETING, RunnerJobState.COMPLETED ])), asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING, RunnerJobState.COMPLETING, RunnerJobState.COMPLETED ])),
@ -101,13 +107,15 @@ runnerJobsRouter.post('/jobs/:jobUUID/update',
asyncMiddleware(updateRunnerJobController) asyncMiddleware(updateRunnerJobController)
) )
runnerJobsRouter.post('/jobs/:jobUUID/error', runnerJobsRouter.post(
'/jobs/:jobUUID/error',
asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])), asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])),
errorRunnerJobValidator, errorRunnerJobValidator,
asyncMiddleware(errorRunnerJob) asyncMiddleware(errorRunnerJob)
) )
runnerJobsRouter.post('/jobs/:jobUUID/success', runnerJobsRouter.post(
'/jobs/:jobUUID/success',
postRunnerJobSuccessVideoFiles, postRunnerJobSuccessVideoFiles,
apiRateLimiter, // Has to be after multer middleware to parse runner token apiRateLimiter, // Has to be after multer middleware to parse runner token
asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])), asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])),
@ -119,7 +127,8 @@ runnerJobsRouter.post('/jobs/:jobUUID/success',
// Controllers for admins // Controllers for admins
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
runnerJobsRouter.post('/jobs/:jobUUID/cancel', runnerJobsRouter.post(
'/jobs/:jobUUID/cancel',
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS), ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(runnerJobGetValidator), asyncMiddleware(runnerJobGetValidator),
@ -127,7 +136,8 @@ runnerJobsRouter.post('/jobs/:jobUUID/cancel',
asyncMiddleware(cancelRunnerJob) asyncMiddleware(cancelRunnerJob)
) )
runnerJobsRouter.get('/jobs', runnerJobsRouter.get(
'/jobs',
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS), ensureUserHasRight(UserRight.MANAGE_RUNNERS),
paginationValidator, paginationValidator,
@ -138,7 +148,8 @@ runnerJobsRouter.get('/jobs',
asyncMiddleware(listRunnerJobs) asyncMiddleware(listRunnerJobs)
) )
runnerJobsRouter.delete('/jobs/:jobUUID', runnerJobsRouter.delete(
'/jobs/:jobUUID',
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS), ensureUserHasRight(UserRight.MANAGE_RUNNERS),
asyncMiddleware(runnerJobGetValidator), asyncMiddleware(runnerJobGetValidator),
@ -172,6 +183,11 @@ async function requestRunnerJob (req: express.Request, res: express.Response) {
})) }))
} }
if (body.version && runner.version !== body.version) {
runner.version = body.version
await runner.save()
}
updateLastRunnerContact(req, runner) updateLastRunnerContact(req, runner)
return res.json(result) return res.json(result)
@ -218,7 +234,10 @@ async function acceptRunnerJob (req: express.Request, res: express.Response) {
updateLastRunnerContact(req, runner) updateLastRunnerContact(req, runner)
logger.info( logger.info(
'Remote runner %s has accepted job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type, 'Remote runner %s has accepted job %s (%s)',
runner.name,
runnerJob.uuid,
runnerJob.type,
lTags(runner.name, runnerJob.uuid, runnerJob.type) lTags(runner.name, runnerJob.uuid, runnerJob.type)
) )
@ -231,7 +250,10 @@ async function abortRunnerJob (req: express.Request, res: express.Response) {
const body: AbortRunnerJobBody = req.body const body: AbortRunnerJobBody = req.body
logger.info( logger.info(
'Remote runner %s is aborting job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type, 'Remote runner %s is aborting job %s (%s)',
runner.name,
runnerJob.uuid,
runnerJob.type,
{ reason: body.reason, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) } { reason: body.reason, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
) )
@ -251,7 +273,10 @@ async function errorRunnerJob (req: express.Request, res: express.Response) {
runnerJob.failures += 1 runnerJob.failures += 1
logger.error( logger.error(
'Remote runner %s had an error with job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type, 'Remote runner %s had an error with job %s (%s)',
runner.name,
runnerJob.uuid,
runnerJob.type,
{ errorMessage: body.message, totalFailures: runnerJob.failures, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) } { errorMessage: body.message, totalFailures: runnerJob.failures, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
) )
@ -294,7 +319,10 @@ async function updateRunnerJobController (req: express.Request, res: express.Res
: undefined : undefined
logger.debug( logger.debug(
'Remote runner %s is updating job %s (%s)', runnerJob.Runner.name, runnerJob.uuid, runnerJob.type, 'Remote runner %s is updating job %s (%s)',
runnerJob.Runner.name,
runnerJob.uuid,
runnerJob.type,
{ body, updatePayload, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) } { body, updatePayload, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
) )
@ -367,7 +395,10 @@ async function postRunnerJobSuccess (req: express.Request, res: express.Response
const resultPayload = jobSuccessPayloadBuilders[runnerJob.type](body.payload, req.files as UploadFiles) const resultPayload = jobSuccessPayloadBuilders[runnerJob.type](body.payload, req.files as UploadFiles)
logger.info( logger.info(
'Remote runner %s is sending success result for job %s (%s)', runnerJob.Runner.name, runnerJob.uuid, runnerJob.type, 'Remote runner %s is sending success result for job %s (%s)',
runnerJob.Runner.name,
runnerJob.uuid,
runnerJob.type,
{ resultPayload, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) } { resultPayload, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
) )

View file

@ -1,4 +1,3 @@
import express from 'express'
import { HttpStatusCode, ListRunnersQuery, RegisterRunnerBody, UserRight } from '@peertube/peertube-models' import { HttpStatusCode, ListRunnersQuery, RegisterRunnerBody, UserRight } from '@peertube/peertube-models'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js' import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { generateRunnerToken } from '@server/helpers/token-generator.js' import { generateRunnerToken } from '@server/helpers/token-generator.js'
@ -18,23 +17,28 @@ import {
registerRunnerValidator registerRunnerValidator
} from '@server/middlewares/validators/runners/index.js' } from '@server/middlewares/validators/runners/index.js'
import { RunnerModel } from '@server/models/runner/runner.js' import { RunnerModel } from '@server/models/runner/runner.js'
import express from 'express'
const lTags = loggerTagsFactory('api', 'runner') const lTags = loggerTagsFactory('api', 'runner')
const manageRunnersRouter = express.Router() const manageRunnersRouter = express.Router()
manageRunnersRouter.post('/register', manageRunnersRouter.post(
'/register',
apiRateLimiter, apiRateLimiter,
asyncMiddleware(registerRunnerValidator), asyncMiddleware(registerRunnerValidator),
asyncMiddleware(registerRunner) asyncMiddleware(registerRunner)
) )
manageRunnersRouter.post('/unregister',
manageRunnersRouter.post(
'/unregister',
apiRateLimiter, apiRateLimiter,
asyncMiddleware(getRunnerFromTokenValidator), asyncMiddleware(getRunnerFromTokenValidator),
asyncMiddleware(unregisterRunner) asyncMiddleware(unregisterRunner)
) )
manageRunnersRouter.delete('/:runnerId', manageRunnersRouter.delete(
'/:runnerId',
apiRateLimiter, apiRateLimiter,
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS), ensureUserHasRight(UserRight.MANAGE_RUNNERS),
@ -42,7 +46,8 @@ manageRunnersRouter.delete('/:runnerId',
asyncMiddleware(deleteRunner) asyncMiddleware(deleteRunner)
) )
manageRunnersRouter.get('/', manageRunnersRouter.get(
'/',
apiRateLimiter, apiRateLimiter,
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_RUNNERS), ensureUserHasRight(UserRight.MANAGE_RUNNERS),
@ -72,6 +77,7 @@ async function registerRunner (req: express.Request, res: express.Response) {
description: body.description, description: body.description,
lastContact: new Date(), lastContact: new Date(),
ip: req.ip, ip: req.ip,
version: body.version,
runnerRegistrationTokenId: res.locals.runnerRegistrationToken.id runnerRegistrationTokenId: res.locals.runnerRegistrationToken.id
}) })

View file

@ -171,3 +171,26 @@ export function toIntArray (value: any) {
return value.map(v => validator.default.toInt(v)) return value.map(v => validator.default.toInt(v))
} }
// ---------------------------------------------------------------------------
export function isStableVersionValid (value: string) {
if (!exists(value)) return false
const parts = (value + '').split('.')
return parts.length === 3 && parts.every(p => validator.default.isInt(p))
}
export function isStableOrUnstableVersionValid (value: string) {
if (!exists(value)) return false
// suffix is beta.x or alpha.x
const [ stable, suffix ] = value.split('-')
if (!isStableVersionValid(stable)) return false
const suffixRegex = /^(rc|alpha|beta)\.\d+$/
if (suffix && !suffixRegex.test(suffix)) return false
return true
}

View file

@ -1,67 +1,46 @@
import validator from 'validator'
import { PluginPackageJSON, PluginType, PluginType_Type } from '@peertube/peertube-models' import { PluginPackageJSON, PluginType, PluginType_Type } from '@peertube/peertube-models'
import validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js' import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
import { isUrlValid } from './activitypub/misc.js' import { isUrlValid } from './activitypub/misc.js'
import { exists, isArray, isSafePath } from './misc.js' import { exists, isArray, isSafePath } from './misc.js'
const PLUGINS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.PLUGINS const PLUGINS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.PLUGINS
function isPluginTypeValid (value: any) { export function isPluginTypeValid (value: any) {
return exists(value) && return exists(value) &&
(value === PluginType.PLUGIN || value === PluginType.THEME) (value === PluginType.PLUGIN || value === PluginType.THEME)
} }
function isPluginNameValid (value: string) { export function isPluginNameValid (value: string) {
return exists(value) && return exists(value) &&
validator.default.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.NAME) && validator.default.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.NAME) &&
validator.default.matches(value, /^[a-z-0-9]+$/) validator.default.matches(value, /^[a-z-0-9]+$/)
} }
function isNpmPluginNameValid (value: string) { export function isNpmPluginNameValid (value: string) {
return exists(value) && return exists(value) &&
validator.default.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.NAME) && validator.default.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.NAME) &&
validator.default.matches(value, /^[a-z\-._0-9]+$/) && validator.default.matches(value, /^[a-z\-._0-9]+$/) &&
(value.startsWith('peertube-plugin-') || value.startsWith('peertube-theme-')) (value.startsWith('peertube-plugin-') || value.startsWith('peertube-theme-'))
} }
function isPluginDescriptionValid (value: string) { export function isPluginDescriptionValid (value: string) {
return exists(value) && validator.default.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.DESCRIPTION) return exists(value) && validator.default.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.DESCRIPTION)
} }
function isPluginStableVersionValid (value: string) { export function isPluginEngineValid (engine: any) {
if (!exists(value)) return false
const parts = (value + '').split('.')
return parts.length === 3 && parts.every(p => validator.default.isInt(p))
}
function isPluginStableOrUnstableVersionValid (value: string) {
if (!exists(value)) return false
// suffix is beta.x or alpha.x
const [ stable, suffix ] = value.split('-')
if (!isPluginStableVersionValid(stable)) return false
const suffixRegex = /^(rc|alpha|beta)\.\d+$/
if (suffix && !suffixRegex.test(suffix)) return false
return true
}
function isPluginEngineValid (engine: any) {
return exists(engine) && exists(engine.peertube) return exists(engine) && exists(engine.peertube)
} }
function isPluginHomepage (value: string) { export function isPluginHomepage (value: string) {
return exists(value) && (!value || isUrlValid(value)) return exists(value) && (!value || isUrlValid(value))
} }
function isPluginBugs (value: string) { export function isPluginBugs (value: string) {
return exists(value) && (!value || isUrlValid(value)) return exists(value) && (!value || isUrlValid(value))
} }
function areStaticDirectoriesValid (staticDirs: any) { export function areStaticDirectoriesValid (staticDirs: any) {
if (!exists(staticDirs) || typeof staticDirs !== 'object') return false if (!exists(staticDirs) || typeof staticDirs !== 'object') return false
for (const key of Object.keys(staticDirs)) { for (const key of Object.keys(staticDirs)) {
@ -71,14 +50,14 @@ function areStaticDirectoriesValid (staticDirs: any) {
return true return true
} }
function areClientScriptsValid (clientScripts: any[]) { export function areClientScriptsValid (clientScripts: any[]) {
return isArray(clientScripts) && return isArray(clientScripts) &&
clientScripts.every(c => { clientScripts.every(c => {
return isSafePath(c.script) && isArray(c.scopes) return isSafePath(c.script) && isArray(c.scopes)
}) })
} }
function areTranslationPathsValid (translations: any) { export function areTranslationPathsValid (translations: any) {
if (!exists(translations) || typeof translations !== 'object') return false if (!exists(translations) || typeof translations !== 'object') return false
for (const key of Object.keys(translations)) { for (const key of Object.keys(translations)) {
@ -88,16 +67,16 @@ function areTranslationPathsValid (translations: any) {
return true return true
} }
function areCSSPathsValid (css: any[]) { export function areCSSPathsValid (css: any[]) {
return isArray(css) && css.every(c => isSafePath(c)) return isArray(css) && css.every(c => isSafePath(c))
} }
function isThemeNameValid (name: string) { export function isThemeNameValid (name: string) {
return name && typeof name === 'string' && return name && typeof name === 'string' &&
(isPluginNameValid(name) || name.startsWith('peertube-core-')) (isPluginNameValid(name) || name.startsWith('peertube-core-'))
} }
function isPackageJSONValid (packageJSON: PluginPackageJSON, pluginType: PluginType_Type) { export function isPackageJSONValid (packageJSON: PluginPackageJSON, pluginType: PluginType_Type) {
let result = true let result = true
const badFields: string[] = [] const badFields: string[] = []
@ -159,20 +138,7 @@ function isPackageJSONValid (packageJSON: PluginPackageJSON, pluginType: PluginT
return { result, badFields } return { result, badFields }
} }
function isLibraryCodeValid (library: any) { export function isLibraryCodeValid (library: any) {
return typeof library.register === 'function' && return typeof library.register === 'function' &&
typeof library.unregister === 'function' typeof library.unregister === 'function'
} }
export {
isPluginTypeValid,
isPackageJSONValid,
isThemeNameValid,
isPluginHomepage,
isPluginStableVersionValid,
isPluginStableOrUnstableVersionValid,
isPluginNameValid,
isPluginDescriptionValid,
isLibraryCodeValid,
isNpmPluginNameValid
}

View file

@ -48,7 +48,7 @@ import { CONFIG, registerConfigChangedHandler } from './config.js'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const LAST_MIGRATION_VERSION = 910 export const LAST_MIGRATION_VERSION = 915
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -0,0 +1,26 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
}): Promise<void> {
const { transaction } = utils
{
await utils.queryInterface.addColumn('runner', 'version', {
type: Sequelize.STRING,
defaultValue: null,
allowNull: true
}, { transaction })
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
down,
up
}

View file

@ -1,12 +1,13 @@
import { isStableOrUnstableVersionValid } from '@server/helpers/custom-validators/misc.js'
import { outputJSON, pathExists } from 'fs-extra/esm' import { outputJSON, pathExists } from 'fs-extra/esm'
import { join } from 'path' import { join } from 'path'
import { execShell } from '../../helpers/core-utils.js' import { execShell } from '../../helpers/core-utils.js'
import { isNpmPluginNameValid, isPluginStableOrUnstableVersionValid } from '../../helpers/custom-validators/plugins.js' import { isNpmPluginNameValid } from '../../helpers/custom-validators/plugins.js'
import { logger } from '../../helpers/logger.js' import { logger } from '../../helpers/logger.js'
import { CONFIG } from '../../initializers/config.js' import { CONFIG } from '../../initializers/config.js'
import { getLatestPluginVersion } from './plugin-index.js' import { getLatestPluginVersion } from './plugin-index.js'
async function installNpmPlugin (npmName: string, versionArg?: string) { export async function installNpmPlugin (npmName: string, versionArg?: string) {
// Security check // Security check
checkNpmPluginNameOrThrow(npmName) checkNpmPluginNameOrThrow(npmName)
if (versionArg) checkPluginVersionOrThrow(versionArg) if (versionArg) checkPluginVersionOrThrow(versionArg)
@ -21,31 +22,22 @@ async function installNpmPlugin (npmName: string, versionArg?: string) {
logger.debug('Added a yarn package.', { yarnStdout: stdout }) logger.debug('Added a yarn package.', { yarnStdout: stdout })
} }
async function installNpmPluginFromDisk (path: string) { export async function installNpmPluginFromDisk (path: string) {
await execYarn('add file:' + path) await execYarn('add file:' + path)
} }
async function removeNpmPlugin (name: string) { export async function removeNpmPlugin (name: string) {
checkNpmPluginNameOrThrow(name) checkNpmPluginNameOrThrow(name)
await execYarn('remove ' + name) await execYarn('remove ' + name)
} }
async function rebuildNativePlugins () { export async function rebuildNativePlugins () {
await execYarn('install --pure-lockfile') await execYarn('install --pure-lockfile')
} }
// ############################################################################ // ############################################################################
export {
installNpmPlugin,
installNpmPluginFromDisk,
rebuildNativePlugins,
removeNpmPlugin
}
// ############################################################################
async function execYarn (command: string) { async function execYarn (command: string) {
try { try {
const pluginDirectory = CONFIG.STORAGE.PLUGINS_DIR const pluginDirectory = CONFIG.STORAGE.PLUGINS_DIR
@ -69,5 +61,5 @@ function checkNpmPluginNameOrThrow (name: string) {
} }
function checkPluginVersionOrThrow (name: string) { function checkPluginVersionOrThrow (name: string) {
if (!isPluginStableOrUnstableVersionValid(name)) throw new Error('Invalid NPM plugin version to install') if (!isStableOrUnstableVersionValid(name)) throw new Error('Invalid NPM plugin version to install')
} }

View file

@ -1,19 +1,21 @@
import { HttpStatusCode, InstallOrUpdatePlugin, PluginType_Type } from '@peertube/peertube-models'
import express from 'express' import express from 'express'
import { body, param, query, ValidationChain } from 'express-validator' import { body, param, query, ValidationChain } from 'express-validator'
import { HttpStatusCode, InstallOrUpdatePlugin, PluginType_Type } from '@peertube/peertube-models'
import { exists, isBooleanValid, isSafePath, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc.js'
import { import {
isNpmPluginNameValid, exists,
isPluginNameValid, isBooleanValid,
isPluginStableOrUnstableVersionValid, isSafePath,
isPluginTypeValid isStableOrUnstableVersionValid,
} from '../../helpers/custom-validators/plugins.js' toBooleanOrNull,
toIntOrNull
} from '../../helpers/custom-validators/misc.js'
import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid } from '../../helpers/custom-validators/plugins.js'
import { CONFIG } from '../../initializers/config.js' import { CONFIG } from '../../initializers/config.js'
import { PluginManager } from '../../lib/plugins/plugin-manager.js' import { PluginManager } from '../../lib/plugins/plugin-manager.js'
import { PluginModel } from '../../models/server/plugin.js' import { PluginModel } from '../../models/server/plugin.js'
import { areValidationErrors } from './shared/index.js' import { areValidationErrors } from './shared/index.js'
const getPluginValidator = (pluginType: PluginType_Type, withVersion = true) => { export const getPluginValidator = (pluginType: PluginType_Type, withVersion = true) => {
const validators: (ValidationChain | express.Handler)[] = [ const validators: (ValidationChain | express.Handler)[] = [
param('pluginName') param('pluginName')
.custom(isPluginNameValid) .custom(isPluginNameValid)
@ -22,7 +24,7 @@ const getPluginValidator = (pluginType: PluginType_Type, withVersion = true) =>
if (withVersion) { if (withVersion) {
validators.push( validators.push(
param('pluginVersion') param('pluginVersion')
.custom(isPluginStableOrUnstableVersionValid) .custom(isStableOrUnstableVersionValid)
) )
} }
@ -53,7 +55,7 @@ const getPluginValidator = (pluginType: PluginType_Type, withVersion = true) =>
]) ])
} }
const getExternalAuthValidator = [ export const getExternalAuthValidator = [
param('authName') param('authName')
.custom(exists), .custom(exists),
@ -82,7 +84,7 @@ const getExternalAuthValidator = [
} }
] ]
const pluginStaticDirectoryValidator = [ export const pluginStaticDirectoryValidator = [
param('staticEndpoint') param('staticEndpoint')
.custom(isSafePath), .custom(isSafePath),
@ -93,7 +95,7 @@ const pluginStaticDirectoryValidator = [
} }
] ]
const listPluginsValidator = [ export const listPluginsValidator = [
query('pluginType') query('pluginType')
.optional() .optional()
.customSanitizer(toIntOrNull) .customSanitizer(toIntOrNull)
@ -110,13 +112,13 @@ const listPluginsValidator = [
} }
] ]
const installOrUpdatePluginValidator = [ export const installOrUpdatePluginValidator = [
body('npmName') body('npmName')
.optional() .optional()
.custom(isNpmPluginNameValid), .custom(isNpmPluginNameValid),
body('pluginVersion') body('pluginVersion')
.optional() .optional()
.custom(isPluginStableOrUnstableVersionValid), .custom(isStableOrUnstableVersionValid),
body('path') body('path')
.optional() .optional()
.custom(isSafePath), .custom(isSafePath),
@ -136,7 +138,7 @@ const installOrUpdatePluginValidator = [
} }
] ]
const uninstallPluginValidator = [ export const uninstallPluginValidator = [
body('npmName') body('npmName')
.custom(isNpmPluginNameValid), .custom(isNpmPluginNameValid),
@ -147,7 +149,7 @@ const uninstallPluginValidator = [
} }
] ]
const existingPluginValidator = [ export const existingPluginValidator = [
param('npmName') param('npmName')
.custom(isNpmPluginNameValid), .custom(isNpmPluginNameValid),
@ -167,7 +169,7 @@ const existingPluginValidator = [
} }
] ]
const updatePluginSettingsValidator = [ export const updatePluginSettingsValidator = [
body('settings') body('settings')
.exists(), .exists(),
@ -178,7 +180,7 @@ const updatePluginSettingsValidator = [
} }
] ]
const listAvailablePluginsValidator = [ export const listAvailablePluginsValidator = [
query('search') query('search')
.optional() .optional()
.exists(), .exists(),
@ -188,7 +190,7 @@ const listAvailablePluginsValidator = [
.custom(isPluginTypeValid), .custom(isPluginTypeValid),
query('currentPeerTubeEngine') query('currentPeerTubeEngine')
.optional() .optional()
.custom(isPluginStableOrUnstableVersionValid), .custom(isStableOrUnstableVersionValid),
(req: express.Request, res: express.Response, next: express.NextFunction) => { (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
@ -200,17 +202,3 @@ const listAvailablePluginsValidator = [
return next() return next()
} }
] ]
// ---------------------------------------------------------------------------
export {
pluginStaticDirectoryValidator,
getPluginValidator,
updatePluginSettingsValidator,
uninstallPluginValidator,
listAvailablePluginsValidator,
existingPluginValidator,
installOrUpdatePluginValidator,
listPluginsValidator,
getExternalAuthValidator
}

View file

@ -1,24 +1,25 @@
import express from 'express' import { forceNumber } from '@peertube/peertube-core-utils'
import { body, param } from 'express-validator' import { HttpStatusCode, RegisterRunnerBody, ServerErrorCode } from '@peertube/peertube-models'
import { isIdValid } from '@server/helpers/custom-validators/misc.js' import { isIdValid, isStableOrUnstableVersionValid } from '@server/helpers/custom-validators/misc.js'
import { import {
isRunnerDescriptionValid, isRunnerDescriptionValid,
isRunnerNameValid, isRunnerNameValid,
isRunnerRegistrationTokenValid, isRunnerRegistrationTokenValid,
isRunnerTokenValid isRunnerTokenValid
} from '@server/helpers/custom-validators/runners/runners.js' } from '@server/helpers/custom-validators/runners/runners.js'
import { RunnerModel } from '@server/models/runner/runner.js'
import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token.js' import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token.js'
import { forceNumber } from '@peertube/peertube-core-utils' import { RunnerModel } from '@server/models/runner/runner.js'
import { HttpStatusCode, RegisterRunnerBody, ServerErrorCode } from '@peertube/peertube-models' import express from 'express'
import { body, param } from 'express-validator'
import { areValidationErrors } from '../shared/utils.js' import { areValidationErrors } from '../shared/utils.js'
const tags = [ 'runner' ] const tags = [ 'runner' ]
const registerRunnerValidator = [ export const registerRunnerValidator = [
body('registrationToken').custom(isRunnerRegistrationTokenValid), body('registrationToken').custom(isRunnerRegistrationTokenValid),
body('name').custom(isRunnerNameValid), body('name').custom(isRunnerNameValid),
body('description').optional().custom(isRunnerDescriptionValid), body('description').optional().custom(isRunnerDescriptionValid),
body('version').optional().custom(isStableOrUnstableVersionValid),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res, { tags })) return if (areValidationErrors(req, res, { tags })) return
@ -50,7 +51,7 @@ const registerRunnerValidator = [
} }
] ]
const deleteRunnerValidator = [ export const deleteRunnerValidator = [
param('runnerId').custom(isIdValid), param('runnerId').custom(isIdValid),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
@ -72,9 +73,8 @@ const deleteRunnerValidator = [
} }
] ]
const getRunnerFromTokenValidator = [ export const getRunnerFromTokenValidator = [
body('runnerToken').custom(isRunnerTokenValid), body('runnerToken').custom(isRunnerTokenValid),
body('jobTypes').optional().isArray(),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res, { tags })) return if (areValidationErrors(req, res, { tags })) return
@ -96,10 +96,13 @@ const getRunnerFromTokenValidator = [
} }
] ]
// --------------------------------------------------------------------------- export const requestRunnerJobValidator = [
body('version').optional().custom(isStableOrUnstableVersionValid),
body('jobTypes').optional().isArray(),
export { (req: express.Request, res: express.Response, next: express.NextFunction) => {
registerRunnerValidator, if (areValidationErrors(req, res, { tags })) return
deleteRunnerValidator,
getRunnerFromTokenValidator return next()
} }
]

View file

@ -1,16 +1,16 @@
import { HttpStatusCode } from '@peertube/peertube-models'
import express from 'express' import express from 'express'
import { param } from 'express-validator' import { param } from 'express-validator'
import { HttpStatusCode } from '@peertube/peertube-models' import { isSafePath, isStableOrUnstableVersionValid } from '../../helpers/custom-validators/misc.js'
import { isSafePath } from '../../helpers/custom-validators/misc.js' import { isPluginNameValid } from '../../helpers/custom-validators/plugins.js'
import { isPluginNameValid, isPluginStableOrUnstableVersionValid } from '../../helpers/custom-validators/plugins.js'
import { PluginManager } from '../../lib/plugins/plugin-manager.js' import { PluginManager } from '../../lib/plugins/plugin-manager.js'
import { areValidationErrors } from './shared/index.js' import { areValidationErrors } from './shared/index.js'
const serveThemeCSSValidator = [ export const serveThemeCSSValidator = [
param('themeName') param('themeName')
.custom(isPluginNameValid), .custom(isPluginNameValid),
param('themeVersion') param('themeVersion')
.custom(isPluginStableOrUnstableVersionValid), .custom(isStableOrUnstableVersionValid),
param('staticEndpoint') param('staticEndpoint')
.custom(isSafePath), .custom(isSafePath),
@ -38,9 +38,3 @@ const serveThemeCSSValidator = [
return next() return next()
} }
] ]
// ---------------------------------------------------------------------------
export {
serveThemeCSSValidator
}

View file

@ -1,10 +1,10 @@
import { Runner } from '@peertube/peertube-models'
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
import { MRunner } from '@server/types/models/runners/index.js'
import { FindOptions } from 'sequelize' import { FindOptions } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Table, UpdatedAt } from 'sequelize-typescript' import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Table, UpdatedAt } from 'sequelize-typescript'
import { MRunner } from '@server/types/models/runners/index.js'
import { Runner } from '@peertube/peertube-models'
import { SequelizeModel, getSort } from '../shared/index.js' import { SequelizeModel, getSort } from '../shared/index.js'
import { RunnerRegistrationTokenModel } from './runner-registration-token.js' import { RunnerRegistrationTokenModel } from './runner-registration-token.js'
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
@Table({ @Table({
tableName: 'runner', tableName: 'runner',
@ -44,6 +44,10 @@ export class RunnerModel extends SequelizeModel<RunnerModel> {
@Column @Column
declare ip: string declare ip: string
@AllowNull(true)
@Column
declare version: string
@CreatedAt @CreatedAt
declare createdAt: Date declare createdAt: Date
@ -114,6 +118,7 @@ export class RunnerModel extends SequelizeModel<RunnerModel> {
ip: this.ip, ip: this.ip,
lastContact: this.lastContact, lastContact: this.lastContact,
version: this.version,
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt updatedAt: this.updatedAt

View file

@ -6,6 +6,7 @@ import {
SettingValue, SettingValue,
type PluginType_Type type PluginType_Type
} from '@peertube/peertube-models' } from '@peertube/peertube-models'
import { isStableOrUnstableVersionValid, isStableVersionValid } from '@server/helpers/custom-validators/misc.js'
import { MPlugin, MPluginFormattable } from '@server/types/models/index.js' import { MPlugin, MPluginFormattable } from '@server/types/models/index.js'
import { FindAndCountOptions, QueryTypes, json } from 'sequelize' import { FindAndCountOptions, QueryTypes, json } from 'sequelize'
import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Table, UpdatedAt } from 'sequelize-typescript' import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Table, UpdatedAt } from 'sequelize-typescript'
@ -13,8 +14,6 @@ import {
isPluginDescriptionValid, isPluginDescriptionValid,
isPluginHomepage, isPluginHomepage,
isPluginNameValid, isPluginNameValid,
isPluginStableOrUnstableVersionValid,
isPluginStableVersionValid,
isPluginTypeValid isPluginTypeValid
} from '../../helpers/custom-validators/plugins.js' } from '../../helpers/custom-validators/plugins.js'
import { SequelizeModel, getSort, throwIfNotValid } from '../shared/index.js' import { SequelizeModel, getSort, throwIfNotValid } from '../shared/index.js'
@ -45,12 +44,12 @@ export class PluginModel extends SequelizeModel<PluginModel> {
declare type: PluginType_Type declare type: PluginType_Type
@AllowNull(false) @AllowNull(false)
@Is('PluginVersion', value => throwIfNotValid(value, isPluginStableOrUnstableVersionValid, 'version')) @Is('PluginVersion', value => throwIfNotValid(value, isStableOrUnstableVersionValid, 'version'))
@Column @Column
declare version: string declare version: string
@AllowNull(true) @AllowNull(true)
@Is('PluginLatestVersion', value => throwIfNotValid(value, isPluginStableVersionValid, 'version')) @Is('PluginLatestVersion', value => throwIfNotValid(value, isStableVersionValid, 'latestVersion'))
@Column @Column
declare latestVersion: string declare latestVersion: string