Skip to content

Commit

Permalink
feat(code): added code/trigger API endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Yury4GL committed Oct 29, 2024
1 parent aa2a1cb commit ffcf193
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 17 deletions.
2 changes: 1 addition & 1 deletion api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>

#default value is 100
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100

#default value is 10
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10
Expand Down
60 changes: 57 additions & 3 deletions api/public/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,36 @@ components:
- runTime
type: object
additionalProperties: false
TriggerCodeResponse:
properties:
sessionId:
type: string
description: "Session ID (SAS WORK folder) used to execute code.\nThis session ID should be used to poll job status."
example: '{ sessionId: ''20241028074744-54132-1730101664824'' }'
required:
- sessionId
type: object
additionalProperties: false
TriggerCodePayload:
properties:
code:
type: string
description: 'Code of program'
example: '* Code HERE;'
runTime:
$ref: '#/components/schemas/RunTimeType'
description: 'runtime for program'
example: sas
expiresAfterMins:
type: number
format: double
description: "Amount of minutes after the completion of the job when the session must be\ndestroyed."
example: 15
required:
- code
- runTime
type: object
additionalProperties: false
MemberType.folder:
enum:
- folder
Expand Down Expand Up @@ -805,6 +835,30 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ExecuteCodePayload'
/SASjsApi/code/trigger:
post:
operationId: TriggerCode
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerCodeResponse'
description: 'Trigger Code on the Specified Runtime'
summary: 'Trigger Code and Return Session Id not awaiting for the job completion'
tags:
- Code
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerCodePayload'
/SASjsApi/drive/deploy:
post:
operationId: Deploy
Expand Down Expand Up @@ -1789,7 +1843,7 @@ paths:
anyOf:
- {type: string}
- {type: string, format: byte}
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts additional URL parameters (converted to session variables)\nand file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
summary: 'Execute a Stored Program, returns _webout and (optionally) log.'
tags:
- STP
Expand All @@ -1798,15 +1852,15 @@ paths:
bearerAuth: []
parameters:
-
description: 'Location of the Stored Program in SASjs Drive'
description: 'Location of code in SASjs Drive'
in: query
name: _program
required: true
schema:
type: string
example: /Projects/myApp/some/program
-
description: 'Optional query param for setting debug mode (returns the session log in the response body)'
description: 'Optional query param for setting debug mode, which will return the session log.'
in: query
name: _debug
required: false
Expand Down
89 changes: 87 additions & 2 deletions api/src/controllers/code.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecutionController } from './internal'
import { ExecutionController, getSessionController } from './internal'
import {
getPreProgramVariables,
getUserAutoExec,
ModeType,
parseLogToArray,
RunTimeType
} from '../utils'

Expand All @@ -22,6 +21,34 @@ interface ExecuteCodePayload {
runTime: RunTimeType
}

interface TriggerCodePayload {
/**
* Code of program
* @example "* Code HERE;"
*/
code: string
/**
* runtime for program
* @example "sas"
*/
runTime: RunTimeType
/**
* Amount of minutes after the completion of the job when the session must be
* destroyed.
* @example 15
*/
expiresAfterMins?: number
}

interface TriggerCodeResponse {
/**
* Session ID (SAS WORK folder) used to execute code.
* This session ID should be used to poll job status.
* @example "{ sessionId: '20241028074744-54132-1730101664824' }"
*/
sessionId: string
}

@Security('bearerAuth')
@Route('SASjsApi/code')
@Tags('Code')
Expand All @@ -44,6 +71,18 @@ export class CodeController {
): Promise<string | Buffer> {
return executeCode(request, body)
}

/**
* Trigger Code on the Specified Runtime
* @summary Trigger Code and Return Session Id not awaiting for the job completion
*/
@Post('/trigger')
public async triggerCode(
@Request() request: express.Request,
@Body() body: TriggerCodePayload
): Promise<TriggerCodeResponse> {
return triggerCode(request, body)
}
}

const executeCode = async (
Expand Down Expand Up @@ -76,3 +115,49 @@ const executeCode = async (
}
}
}

const triggerCode = async (
req: express.Request,
{ code, runTime, expiresAfterMins }: TriggerCodePayload
): Promise<{ sessionId: string }> => {
const { user } = req
const userAutoExec =
process.env.MODE === ModeType.Server
? user?.autoExec
: await getUserAutoExec()

// get session controller based on runTime
const sessionController = getSessionController(runTime)

// get session
const session = await sessionController.getSession()

// add expiresAfterMins to session if provided
if (expiresAfterMins) {
// expiresAfterMins.used is set initially to false
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
}

try {
// call executeProgram method of ExecutionController without awaiting
new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime,
includePrintOutput: true,
session // session is provided
})

// return session id
return { sessionId: session.id }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}
32 changes: 22 additions & 10 deletions api/src/controllers/internal/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import {
createFile,
fileExists,
generateTimestamp,
readFile,
isWindows
readFile
} from '@sasjs/utils'

const execFilePromise = promisify(execFile)
Expand Down Expand Up @@ -190,20 +189,33 @@ ${autoExecContent}`
}

private scheduleSessionDestroy(session: Session) {
setTimeout(
async () => {
if (session.inUse) {
// adding 10 more minutes
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
setTimeout(async () => {
if (session.inUse) {
// adding 10 more minutes
const newDeathTimeStamp =
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()

this.scheduleSessionDestroy(session)
} else {
const { expiresAfterMins } = session

// delay session destroy if expiresAfterMins present
if (expiresAfterMins && !expiresAfterMins.used) {
// calculate session death time using expiresAfterMins
const newDeathTimeStamp =
parseInt(session.deathTimeStamp) + expiresAfterMins.mins * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()

// set expiresAfterMins to true to avoid using it again
session.expiresAfterMins!.used = true

this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
},
parseInt(session.deathTimeStamp) - new Date().getTime() - 100
)
}
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
}
}

Expand Down
20 changes: 19 additions & 1 deletion api/src/routes/api/code.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import express from 'express'
import { runCodeValidation } from '../../utils'
import { runCodeValidation, triggerCodeValidation } from '../../utils'
import { CodeController } from '../../controllers/'

const runRouter = express.Router()
Expand Down Expand Up @@ -28,4 +28,22 @@ runRouter.post('/execute', async (req, res) => {
}
})

runRouter.post('/trigger', async (req, res) => {
const { error, value: body } = triggerCodeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)

try {
const response = await controller.triggerCode(req, body)

res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code

delete err.code

res.status(statusCode).send(err)
}
})

export default runRouter
1 change: 1 addition & 0 deletions api/src/types/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface Session {
consumed: boolean
completed: boolean
crashed?: string
expiresAfterMins?: { mins: number; used: boolean }
}
7 changes: 7 additions & 0 deletions api/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ export const runCodeValidation = (data: any): Joi.ValidationResult =>
runTime: Joi.string().valid(...process.runTimes)
}).validate(data)

export const triggerCodeValidation = (data: any): Joi.ValidationResult =>
Joi.object({
code: Joi.string().required(),
runTime: Joi.string().valid(...process.runTimes),
expiresAfterMins: Joi.number().greater(0)
}).validate(data)

export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
Joi.object({
_program: Joi.string().required(),
Expand Down

0 comments on commit ffcf193

Please sign in to comment.