diff --git a/api-backend/.env.example b/api-backend/.env.example index 0012702..84675ba 100644 --- a/api-backend/.env.example +++ b/api-backend/.env.example @@ -15,6 +15,22 @@ DB_DATABASE= REDIS_HOST= REDIS_PORT= +# Cloud (ovh or aws) +CLOUD_PROVIDER= + +#OVH +OVH_KUBECONFIG_PATH= +OVH_CLUSTER_NAMESPACE= + +OVH_IMAGE_GOOGLE= +OVH_IMAGE_TEAMS= +OVH_IMAGE_ZOOM= +OVH_IMAGE_PULL_SECRET= + +#OVH bot env +OVH_ENVIRONMENT_NAME= +OVH_DEBUG= +OVH_HIGHLIGHT_PROJECT_ID= # AWS AWS_ACCESS_KEY= diff --git a/api-backend/.gitignore b/api-backend/.gitignore index 6e1e8a4..d9ecbbe 100644 --- a/api-backend/.gitignore +++ b/api-backend/.gitignore @@ -55,3 +55,5 @@ pids # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +kubeconfig.yml diff --git a/api-backend/Dockerfile b/api-backend/Dockerfile index 627cce2..713b9bb 100644 --- a/api-backend/Dockerfile +++ b/api-backend/Dockerfile @@ -1,5 +1,5 @@ # Use the official Node.js 20 image as a base image -FROM public.ecr.aws/docker/library/node:20.11.0 +FROM public.ecr.aws/docker/library/node:22.15.1 # Set the working directory WORKDIR /app diff --git a/api-backend/package.json b/api-backend/package.json index 6bd0c80..6a54991 100644 --- a/api-backend/package.json +++ b/api-backend/package.json @@ -24,6 +24,7 @@ "@aws-sdk/s3-request-presigner": "^3.758.0", "@grpc/grpc-js": "^1.12.6", "@grpc/proto-loader": "^0.7.13", + "@kubernetes/client-node": "^1.3.0", "@nestjs/bullmq": "^11.0.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^4.0.1", diff --git a/api-backend/src/app.module.ts b/api-backend/src/app.module.ts index b5c5339..3238e36 100644 --- a/api-backend/src/app.module.ts +++ b/api-backend/src/app.module.ts @@ -13,6 +13,8 @@ import { AwsModule } from './aws/aws.module'; import { BullModule } from '@nestjs/bullmq'; import { CronJobModule } from './cron-job/cron-job.module'; import { GrpcModule } from './grpc/grpc.module'; +import { OvhModule } from './ovh/ovh.module'; +import { CloudModule } from './cloud/cloud.module'; @Module({ imports: [ @@ -38,6 +40,8 @@ import { GrpcModule } from './grpc/grpc.module'; AwsModule, CronJobModule, GrpcModule, + OvhModule, + CloudModule, ], controllers: [AppController], providers: [ diff --git a/api-backend/src/aws/ecs.service.ts b/api-backend/src/aws/ecs.service.ts index 444ab4b..4be304b 100644 --- a/api-backend/src/aws/ecs.service.ts +++ b/api-backend/src/aws/ecs.service.ts @@ -10,10 +10,8 @@ import { ListTasksCommandOutput, RunTaskCommand, RunTaskCommandInput, - RunTaskCommandOutput, StopTaskCommand, StopTaskCommandInput, - StopTaskCommandOutput, Task, } from '@aws-sdk/client-ecs'; @@ -31,6 +29,10 @@ import { InjectQueue } from '@nestjs/bullmq'; import { Bot, ExecutionStatusLogEnum } from 'src/database/models/bot.model'; import { InjectModel } from '@nestjs/sequelize'; import { BotService } from 'src/bot/bot.service'; +import { + ICloudService, + IRunTaskInput, +} from 'src/cloud/cloud-service.interface'; interface TaskInfo { taskId?: string; lastStatus?: string; @@ -43,7 +45,7 @@ interface TaskInfo { } @Injectable() -export class ECSClientService { +export class ECSClientService implements ICloudService { private readonly ecsClient: ECSClient; private readonly clusterName: string; private readonly launchType: LaunchType = 'FARGATE'; @@ -87,12 +89,12 @@ export class ECSClientService { } } - async runTask( - taskDefinition: string, - containerName: string, - command: string[], + async runTask({ + containerName, + command, + taskDefinition, taskCount = 1, - ): Promise { + }: IRunTaskInput): Promise { try { const input: RunTaskCommandInput = { cluster: this.clusterName, @@ -118,14 +120,21 @@ export class ECSClientService { const commandInstance = new RunTaskCommand(input); - return this.ecsClient.send(commandInstance); + const res = await this.ecsClient.send(commandInstance); + + if (!(res.hasOwnProperty('tasks') && res['tasks'].length > 0)) + throw new Error('Failed to start ECS task.'); + + const taskArn = res['tasks'][0]['taskArn']; + + return this.getTaskIdByTaskArn(taskArn); } catch (error) { Logger.error(`Failed to run task: ${error}`); throw error; } } - async stopTask(taskId: string): Promise { + async stopTask(taskId: string): Promise { try { const input: StopTaskCommandInput = { cluster: this.clusterName, @@ -133,7 +142,7 @@ export class ECSClientService { }; const command = new StopTaskCommand(input); - return this.ecsClient.send(command); + await this.ecsClient.send(command); } catch (error) { Logger.error(`Failed to stop task: ${error}`); throw error; @@ -242,7 +251,11 @@ export class ECSClientService { const containerName = taskInfo?.containerOverrides?.[0]?.name; const command = taskInfo.containerOverrides?.[0]?.command; - const res = await this.runTask(taskDefinition, containerName, command); + const res = await this.runTask({ + taskDefinition, + containerName, + command, + }); if (!(res.hasOwnProperty('tasks') && res['tasks'].length > 0)) throw new Error('Failed to start ECS task.'); diff --git a/api-backend/src/bot/bot.module.ts b/api-backend/src/bot/bot.module.ts index bc39733..51ce0c7 100644 --- a/api-backend/src/bot/bot.module.ts +++ b/api-backend/src/bot/bot.module.ts @@ -5,11 +5,13 @@ import { AwsModule } from 'src/aws/aws.module'; import { Bot } from 'src/database/models/bot.model'; import { BotController } from './bot.controller'; import { BotService } from './bot.service'; +import { CloudModule } from 'src/cloud/cloud.module'; @Module({ imports: [ SequelizeModule.forFeature([Bot]), forwardRef(() => AwsModule), AuthModule, + CloudModule, ], providers: [BotService], controllers: [BotController], diff --git a/api-backend/src/bot/bot.service.ts b/api-backend/src/bot/bot.service.ts index 098a278..1f721d2 100644 --- a/api-backend/src/bot/bot.service.ts +++ b/api-backend/src/bot/bot.service.ts @@ -1,7 +1,5 @@ import { BadRequestException, - forwardRef, - Inject, Injectable, InternalServerErrorException, NotFoundException, @@ -12,7 +10,7 @@ import * as moment from 'moment-timezone'; import { lastValueFrom } from 'rxjs'; import { Op } from 'sequelize'; import { AwsService } from 'src/aws/aws.service'; -import { ECSClientService } from 'src/aws/ecs.service'; +import { CloudService } from 'src/cloud/cloud.service'; import { ApiKey } from 'src/database/models/api-key.model'; import { Bot, @@ -28,7 +26,6 @@ import { TranscriptionLogResponse, } from 'src/interfaces/proto-generated/transcript_management'; import { v4 as uuidv4 } from 'uuid'; -import { TASK_STOPPED_ERROR_CODES } from 'src/constants/ecs'; @Injectable() export class BotService { @@ -36,8 +33,7 @@ export class BotService { @InjectModel(Bot) private botModel: typeof Bot, private readonly awsService: AwsService, - @Inject(forwardRef(() => ECSClientService)) - private readonly ecsService: ECSClientService, + private readonly cloudService: CloudService, private readonly configService: ConfigService, private readonly workerService: WorkerService, ) {} @@ -199,7 +195,7 @@ export class BotService { async leaveCall(botId: string): Promise { const bot = await this.botModel.findByPk(botId); if (!bot) throw new NotFoundException('Bot not found'); - await this.ecsService.stopTask(bot.taskId); + await this.cloudService.stopTask(bot.taskId); await bot.update({ status: ExecutionStatusLogEnum.STOPPED }); await bot.reload(); @@ -254,10 +250,6 @@ export class BotService { botName, waitingTime = 8100, ) { - const taskDefinition = this.configService.get( - 'aws.ecsTaskDefinitionGoogle', - ); - const containerName = this.configService.get('aws.ecsContainerNameGoogle'); const command = [ '/bin/bash', '-c', @@ -265,18 +257,7 @@ export class BotService { '--max-waiting-time', waitingTime.toString(), ]; - const res = await this.ecsService.runTask( - taskDefinition, - containerName, - command, - ); - - if (!(res.hasOwnProperty('tasks') && res['tasks'].length > 0)) - throw new Error('Failed to start ECS task.'); - - const taskArn = res['tasks'][0]['taskArn']; - const splittedTaskArn = taskArn.split('/'); - const taskId = splittedTaskArn[splittedTaskArn.length - 1]; + const taskId = await this.cloudService.runGoogleBotTask(command); return taskId; } @@ -287,8 +268,6 @@ export class BotService { botName, waitingTime = 8100, ) { - const taskDefinition = this.configService.get('aws.ecsTaskDefinitionTeams'); - const containerName = this.configService.get('aws.ecsContainerNameTeams'); const command = [ '/bin/bash', '-c', @@ -296,18 +275,7 @@ export class BotService { '--max-waiting-time', waitingTime.toString(), ]; - const res = await this.ecsService.runTask( - taskDefinition, - containerName, - command, - ); - - if (!(res.hasOwnProperty('tasks') && res['tasks'].length > 0)) - throw new Error('Failed to start ECS task.'); - - const taskArn = res['tasks'][0]['taskArn']; - const splittedTaskArn = taskArn.split('/'); - const taskId = splittedTaskArn[splittedTaskArn.length - 1]; + const taskId = await this.cloudService.runTeemsBotTask(command); return taskId; } @@ -318,8 +286,6 @@ export class BotService { botName, waitingTime = 8100, ) { - const taskDefinition = this.configService.get('aws.ecsTaskDefinitionZoom'); - const containerName = this.configService.get('aws.ecsContainerNameZoom'); const command = [ '/bin/bash', '-c', @@ -330,18 +296,7 @@ export class BotService { }" --max-waiting-time ${waitingTime.toString()}`, ]; - const res = await this.ecsService.runTask( - taskDefinition, - containerName, - command, - ); - - if (!(res.hasOwnProperty('tasks') && res['tasks'].length > 0)) - throw new Error('Failed to start ECS task.'); - - const taskArn = res['tasks'][0]['taskArn']; - const splittedTaskArn = taskArn.split('/'); - const taskId = splittedTaskArn[splittedTaskArn.length - 1]; + const taskId = await this.cloudService.runZoomBotTask(command); return taskId; } @@ -392,38 +347,6 @@ export class BotService { }; } - async findBotsWithNonErrorFailures(): Promise { - const bots = await this.botModel.findAll({ - where: { - status: { [Op.not]: ExecutionStatusLogEnum.FAILED }, - taskId: { [Op.not]: null }, - // Only get bots from the last 24 hours - createdAt: { - [Op.gte]: moment().subtract(24, 'hours').toDate(), - }, - }, - include: [{ model: ApiKey, attributes: ['userId'] }], - }); - - // Filter bots to only those that failed for non-error reasons - const results = []; - for (const bot of bots) { - try { - const taskInfo = await this.ecsService.healthCheckTask(bot.taskId); - if ( - taskInfo?.lastStatus === 'STOPPED' && - !TASK_STOPPED_ERROR_CODES.includes(taskInfo?.stopCode) - ) { - results.push(bot); - } - } catch (error) { - console.error(`Error checking task status for bot ${bot.id}:`, error); - } - } - - return results; - } - async triggerTranscriptGeneration(botId: string): Promise { const bot = await this.botModel.findByPk(botId, { include: [{ model: ApiKey, attributes: ['userId'] }], diff --git a/api-backend/src/cloud/cloud-service.interface.ts b/api-backend/src/cloud/cloud-service.interface.ts new file mode 100644 index 0000000..d22d025 --- /dev/null +++ b/api-backend/src/cloud/cloud-service.interface.ts @@ -0,0 +1,21 @@ +export interface ICloudService { + runTask(input: IRunTaskInput): Promise; + stopTask(taskId: string): Promise; + syncTaskStatus(): Promise; +} + +export interface IRunTaskInput { + command: string[]; + containerName?: string; + image?: string; + taskDefinition?: string; + taskCount?: number; + resourceRequests?: { + cpu: string; + memory: string; + }; + resourceLimits?: { + cpu: string; + memory: string; + }; +} diff --git a/api-backend/src/cloud/cloud.module.ts b/api-backend/src/cloud/cloud.module.ts new file mode 100644 index 0000000..914073f --- /dev/null +++ b/api-backend/src/cloud/cloud.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AwsModule } from 'src/aws/aws.module'; +import { OvhModule } from 'src/ovh/ovh.module'; +import { CloudService } from './cloud.service'; + +@Module({ + imports: [OvhModule, AwsModule], + providers: [CloudService], + exports: [CloudService], +}) +export class CloudModule {} diff --git a/api-backend/src/cloud/cloud.service.spec.ts b/api-backend/src/cloud/cloud.service.spec.ts new file mode 100644 index 0000000..26c5532 --- /dev/null +++ b/api-backend/src/cloud/cloud.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CloudService } from './cloud.service'; + +describe('CloudService', () => { + let service: CloudService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CloudService], + }).compile(); + + service = module.get(CloudService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/api-backend/src/cloud/cloud.service.ts b/api-backend/src/cloud/cloud.service.ts new file mode 100644 index 0000000..46f34ff --- /dev/null +++ b/api-backend/src/cloud/cloud.service.ts @@ -0,0 +1,106 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ECSClientService } from 'src/aws/ecs.service'; +import { OvhService } from 'src/ovh/ovh.service'; +import { ICloudService } from './cloud-service.interface'; + +enum CloudProvider { + OVH = 'ovh', + AWS = 'aws', +} + +@Injectable() +export class CloudService { + private readonly cloudService: ICloudService; + private readonly cloudProvider: CloudProvider = CloudProvider.OVH; + + constructor( + @Inject(forwardRef(() => OvhService)) + private readonly ovhService: OvhService, + @Inject(forwardRef(() => ECSClientService)) + private readonly ecsService: ECSClientService, + private readonly configService: ConfigService, + ) { + this.cloudProvider = this.configService.get('cloud.provider'); + this.cloudService = + this.configService.get('cloud.provider') === CloudProvider.OVH + ? this.ovhService + : this.ecsService; + } + + async runGoogleBotTask(command: string[]): Promise { + if (this.cloudProvider === CloudProvider.OVH) { + const image = this.configService.get('ovh.imageGoogle'); + return this.ovhService.runTask({ + command, + image, + }); + } + + if (this.cloudProvider === CloudProvider.AWS) { + const taskDefinition = this.configService.get( + 'aws.ecsTaskDefinitionGoogle', + ); + const containerName = this.configService.get( + 'aws.ecsContainerNameGoogle', + ); + return this.ecsService.runTask({ + command, + taskDefinition, + containerName, + }); + } + } + + async runTeemsBotTask(command: string[]): Promise { + if (this.cloudProvider === CloudProvider.OVH) { + const image = this.configService.get('ovh.imageTeems'); + return this.ovhService.runTask({ + command, + image, + }); + } + + if (this.cloudProvider === CloudProvider.AWS) { + const taskDefinition = this.configService.get( + 'aws.ecsTaskDefinitionTeams', + ); + const containerName = this.configService.get('aws.ecsContainerNameTeams'); + return this.ecsService.runTask({ + command, + taskDefinition, + containerName, + }); + } + } + + async runZoomBotTask(command: string[]): Promise { + if (this.cloudProvider === CloudProvider.OVH) { + const image = this.configService.get('ovh.imageZoom'); + return this.ovhService.runTask({ + command, + image, + }); + } + + if (this.cloudProvider === CloudProvider.AWS) { + const taskDefinition = this.configService.get( + 'aws.ecsTaskDefinitionZoom', + ); + const containerName = this.configService.get('aws.ecsContainerNameZoom'); + return this.ecsService.runTask({ + command, + taskDefinition, + containerName, + }); + } + } + + async stopTask(taskId: string): Promise { + return this.cloudService.stopTask(taskId); + } + + async syncTaskStatus(): Promise { + return this.cloudService.syncTaskStatus(); + } +} diff --git a/api-backend/src/config/configuration.ts b/api-backend/src/config/configuration.ts index 1f30430..67f9141 100644 --- a/api-backend/src/config/configuration.ts +++ b/api-backend/src/config/configuration.ts @@ -14,6 +14,22 @@ export default () => ({ grpc: { workerBackendUrl: process.env.WORKER_BACKEND_GRPC_URL, }, + cloud: { + provider: process.env.CLOUD_PROVIDER, + }, + ovh: { + imageGoogle: process.env.OVH_IMAGE_GOOGLE, + imageTeams: process.env.OVH_IMAGE_TEAMS, + imageZoom: process.env.OVH_IMAGE_ZOOM, + imagePullSecret: process.env.OVH_IMAGE_PULL_SECRET, + kubeConfigPath: process.env.OVH_KUBECONFIG_PATH, + clusterNamespace: process.env.OVH_CLUSTER_NAMESPACE, + botEnv: { + ENVIRONMENT_NAME: process.env.OVH_ENVIRONMENT_NAME, + DEBUG: process.env.OVH_DEBUG, + HIGHLIGHT_PROJECT_ID: process.env.OVH_HIGHLIGHT_PROJECT_ID, + }, + }, aws: { accessKey: process.env.AWS_ACCESS_KEY, secretKey: process.env.AWS_SECRET_KEY, diff --git a/api-backend/src/constants/bull-queue.ts b/api-backend/src/constants/bull-queue.ts index f48b79e..f8b9fab 100644 --- a/api-backend/src/constants/bull-queue.ts +++ b/api-backend/src/constants/bull-queue.ts @@ -1,2 +1,5 @@ export const ECS_TASK_QUEUE = 'ECS_TASK_QUEUE'; export const ECS_TASK_INITIATE_FAILED_TASK = 'ECS_TASK_INITIATE_FAILED_TASK'; + +export const OVH_TASK_QUEUE = 'OVH_TASK_QUEUE'; +export const OVH_TASK_INITIATE_FAILED_TASK = 'OVH_TASK_INITIATE_FAILED_TASK'; diff --git a/api-backend/src/cron-job/cron-job.module.ts b/api-backend/src/cron-job/cron-job.module.ts index bca2600..add63d6 100644 --- a/api-backend/src/cron-job/cron-job.module.ts +++ b/api-backend/src/cron-job/cron-job.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { CronJobService } from './cron-job.service'; import { ScheduleModule } from '@nestjs/schedule'; -import { AwsModule } from 'src/aws/aws.module'; import { BotModule } from 'src/bot/bot.module'; import { GrpcModule } from 'src/grpc/grpc.module'; +import { CloudModule } from 'src/cloud/cloud.module'; @Module({ - imports: [ScheduleModule.forRoot(), AwsModule, BotModule, GrpcModule], + imports: [ScheduleModule.forRoot(), CloudModule, BotModule, GrpcModule], providers: [CronJobService], exports: [CronJobService], }) diff --git a/api-backend/src/cron-job/cron-job.service.ts b/api-backend/src/cron-job/cron-job.service.ts index 6260039..d28ef27 100644 --- a/api-backend/src/cron-job/cron-job.service.ts +++ b/api-backend/src/cron-job/cron-job.service.ts @@ -1,43 +1,41 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { lastValueFrom } from 'rxjs'; -import { ECSClientService } from 'src/aws/ecs.service'; import { BotService } from 'src/bot/bot.service'; +import { CloudService } from 'src/cloud/cloud.service'; import { WorkerService } from 'src/grpc/worker.service'; @Injectable() export class CronJobService { + private readonly logger = new Logger(CronJobService.name); constructor( - private readonly ecsService: ECSClientService, + private readonly cloudService: CloudService, private readonly botService: BotService, private readonly workerService: WorkerService, ) {} @Cron(CronExpression.EVERY_5_MINUTES) async runEvery5Minutes(): Promise { - await this.ecsService.syncTaskStatus(); - } - - @Cron(CronExpression.EVERY_10_MINUTES) - async runEvery10Minutes(): Promise { - await this.botService.initiateScheduledBot(); - } - - @Cron(CronExpression.EVERY_10_MINUTES) - async checkBotStatusAndProcess(): Promise { try { - console.log('Checking bot status and processing'); + this.logger.log('Checking bot status and processing'); // First check if the worker service is healthy const healthCheck = await lastValueFrom(this.workerService.healthCheck()); if (healthCheck?.ServingStatus !== '200') { - console.log('Worker service is not healthy, skipping bot status check'); + this.logger.log( + 'Worker service is not healthy, skipping bot status check', + ); return; } // Get bot status through ECS service - await this.ecsService.syncTaskStatus(); + await this.cloudService.syncTaskStatus(); } catch (error) { - console.error('Error in bot status check cron job:', error); + this.logger.error('Error in bot status check cron job:', error); } } + + @Cron(CronExpression.EVERY_10_MINUTES) + async runEvery10Minutes(): Promise { + await this.botService.initiateScheduledBot(); + } } diff --git a/api-backend/src/interfaces/ovh/job-Info.ts b/api-backend/src/interfaces/ovh/job-Info.ts new file mode 100644 index 0000000..e5b8b01 --- /dev/null +++ b/api-backend/src/interfaces/ovh/job-Info.ts @@ -0,0 +1,8 @@ +export interface IJobInfo { + jobName?: string | undefined; + status?: string | undefined; + completionTime?: string | undefined; + failureReason?: string | undefined; + podName?: string | undefined; + containerOverrides?: any[] | undefined; +} diff --git a/api-backend/src/ovh/ovh.module.ts b/api-backend/src/ovh/ovh.module.ts new file mode 100644 index 0000000..6f48406 --- /dev/null +++ b/api-backend/src/ovh/ovh.module.ts @@ -0,0 +1,19 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { OvhService } from './ovh.service'; +import { BullModule } from '@nestjs/bullmq'; +import { OVH_TASK_QUEUE } from 'src/constants/bull-queue'; +import { BotModule } from 'src/bot/bot.module'; +import { ConfigModule } from '@nestjs/config'; + +@Module({ + imports: [ + ConfigModule, + forwardRef(() => BotModule), + BullModule.registerQueue({ + name: OVH_TASK_QUEUE, + }), + ], + providers: [OvhService], + exports: [OvhService], +}) +export class OvhModule {} diff --git a/api-backend/src/ovh/ovh.service.spec.ts b/api-backend/src/ovh/ovh.service.spec.ts new file mode 100644 index 0000000..0d8297d --- /dev/null +++ b/api-backend/src/ovh/ovh.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { OvhService } from './ovh.service'; + +describe('OvhService', () => { + let service: OvhService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [OvhService], + }).compile(); + + service = module.get(OvhService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/api-backend/src/ovh/ovh.service.ts b/api-backend/src/ovh/ovh.service.ts new file mode 100644 index 0000000..5ca0a85 --- /dev/null +++ b/api-backend/src/ovh/ovh.service.ts @@ -0,0 +1,272 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { + ICloudService, + IRunTaskInput, +} from 'src/cloud/cloud-service.interface'; +import * as k8s from '@kubernetes/client-node'; +import { Op } from 'sequelize'; +import { + OVH_TASK_INITIATE_FAILED_TASK, + OVH_TASK_QUEUE, +} from 'src/constants/bull-queue'; +import * as moment from 'moment'; + +import { InjectModel } from '@nestjs/sequelize'; +import { Bot, ExecutionStatusLogEnum } from 'src/database/models/bot.model'; +import { ConfigService } from '@nestjs/config'; +import { Queue } from 'bullmq'; +import { InjectQueue } from '@nestjs/bullmq'; +import { v4 as uuidv4 } from 'uuid'; +import { IJobInfo } from 'src/interfaces/ovh/job-Info'; +import { BotService } from 'src/bot/bot.service'; + +@Injectable() +export class OvhService implements ICloudService { + private readonly k8sApi: k8s.BatchV1Api; + private readonly k8sCoreApi: k8s.CoreV1Api; + private readonly namespace: string; + private readonly kubeConfig: k8s.KubeConfig; + + constructor( + @InjectModel(Bot) + private readonly botModel: typeof Bot, + @InjectQueue(OVH_TASK_QUEUE) + private readonly ovhTaskQueue: Queue, + private readonly configService: ConfigService, + @Inject(forwardRef(() => BotService)) + private readonly botService: BotService, + ) { + this.kubeConfig = new k8s.KubeConfig(); + + // Load configuration - can be from file, cluster, or OVH credentials + if (this.configService.get('ovh.kubeConfigPath')) { + this.kubeConfig.loadFromFile( + this.configService.get('ovh.kubeConfigPath'), + ); + } else { + // Load from default locations or cluster service account + this.kubeConfig.loadFromDefault(); + } + + this.k8sApi = this.kubeConfig.makeApiClient(k8s.BatchV1Api); + this.k8sCoreApi = this.kubeConfig.makeApiClient(k8s.CoreV1Api); + this.namespace = + this.configService.get('ovh.clusterNamespace') || 'default'; + } + + async runTask({ + image, + command, + taskCount = 1, + resourceRequests, + resourceLimits, + }: IRunTaskInput): Promise { + try { + const jobName = uuidv4(); + const jobManifest: k8s.V1Job = { + apiVersion: 'batch/v1', + kind: 'Job', + metadata: { + name: jobName, + namespace: this.namespace, + }, + spec: { + completions: taskCount, + parallelism: taskCount, + template: { + spec: { + restartPolicy: 'Never', + // Add image pull secrets for OVH private registry + imagePullSecrets: [ + { + name: this.configService.get('ovh.imagePullSecret'), + }, + ], + containers: [ + { + name: 'cuemeet-bot', + // Use OVH private registry format + image: image, // e.g., 'your-registry.gra.cloud.ovh.net/your-namespace/cuecard-google-bot-staging:latest' + command: command, + // Environment variables from ECS task definition + env: [ + { + name: 'ENVIRONMENT_NAME', + value: this.configService.get( + 'ovh.botEnv.ENVIRONMENT_NAME', + ), + }, + { + name: 'DEBUG', + value: this.configService.get('ovh.botEnv.DEBUG'), + }, + { + name: 'HIGHLIGHT_PROJECT_ID', + value: this.configService.get( + 'ovh.botEnv.HIGHLIGHT_PROJECT_ID', + ), + }, + ], + resources: { + // Convert ECS resources (1024 CPU units = 1 vCPU, 2048 MB = 2Gi) + requests: resourceRequests || { + cpu: '1000m', // 1 vCPU (ECS: 1024 CPU units) + memory: '2Gi', // 2GB (ECS: 2048 MB) + }, + limits: resourceLimits || { + cpu: '1000m', // Match requests for guaranteed QoS + memory: '2Gi', // Match requests for guaranteed QoS + }, + }, + }, + ], + }, + }, + backoffLimit: 4, + activeDeadlineSeconds: 3600, + }, + }; + + await this.k8sApi.createNamespacedJob({ + namespace: this.namespace, + body: jobManifest, + }); + + return jobName; + } catch (error) { + console.error(`Failed to run job: ${error}`); + throw error; + } + } + async stopTask(taskId: string): Promise { + try { + await this.k8sApi.deleteNamespacedJob({ + name: taskId, + namespace: this.namespace, + propagationPolicy: 'Background', + }); + } catch (error) { + console.error(`Failed to stop job: ${error}`); + throw error; + } + } + + async getTaskInfo(taskId: string): Promise { + try { + const response = await this.k8sApi.readNamespacedJob({ + name: taskId, + namespace: this.namespace, + }); + return response; + } catch (error) { + console.error(`Failed to get job info: ${error}`); + throw error; + } + } + async healthCheckTask(taskId: string): Promise { + try { + const jobResponse = await this.k8sApi.readNamespacedJob({ + name: taskId, + namespace: this.namespace, + }); + const job = jobResponse; + + // Get pods associated with this job + const podsResponse = await this.k8sCoreApi.listNamespacedPod({ + namespace: this.namespace, + labelSelector: `job-name=${taskId}`, + }); + + const pods = podsResponse.items; + const latestPod = pods.length > 0 ? pods[pods.length - 1] : null; + + let status = 'Unknown'; + let failureReason = ''; + + if (job.status?.succeeded) { + status = 'Succeeded'; + } else if (job.status?.failed) { + status = 'Failed'; + failureReason = + job.status.conditions?.[0]?.message || 'Unknown failure'; + } else if (job.status?.active) { + status = 'Running'; + } + + return { + jobName: job.metadata?.name, + status, + completionTime: job.status?.completionTime?.toISOString(), + failureReason, + podName: latestPod?.metadata?.name, + containerOverrides: job.spec?.template?.spec?.containers, + }; + } catch (error) { + console.error(`Failed to perform health check on job: ${error}`); + throw error; + } + } + async syncTaskStatus(): Promise { + console.log('Starting Kubernetes job status synchronization...'); + const bots = await this.botModel.findAll({ + where: { + taskId: { [Op.not]: null as unknown }, + status: ExecutionStatusLogEnum.STARTED, + createdAt: { + [Op.between]: [ + moment().startOf('day').toISOString(), + moment().endOf('day').toISOString(), + ], + }, + }, + }); + + for (const bot of bots) { + try { + const jobInfo = await this.healthCheckTask(bot.taskId); + + if (jobInfo?.status === 'Failed') { + await bot.update({ + status: ExecutionStatusLogEnum.FAILED, + }); + if (this.configService.get('nodeEnv') === 'production') { + await this.ovhTaskQueue.add( + OVH_TASK_INITIATE_FAILED_TASK, + { botId: bot.id }, + { + removeOnComplete: true, + removeOnFail: true, + }, + ); + } + } else if (jobInfo?.status === 'Succeeded') { + await bot.update({ + status: ExecutionStatusLogEnum.COMPLETED, + }); + await this.botService.triggerTranscriptGeneration(bot.id); + } + } catch (error) { + await bot.update({ + status: ExecutionStatusLogEnum.FAILED, + }); + console.error(`Failed to sync job(${bot.id}) status: ${error}`); + } + } + } + async reInitiateTask(taskId: string): Promise { + // Get original job configuration to recreate + const originalJob = await this.getTaskInfo(taskId); + if (!originalJob?.spec?.template?.spec?.containers?.[0]) { + throw new Error('Could not retrieve original job configuration'); + } + + const container = originalJob.spec.template.spec.containers[0]; + + const newJobName = await this.runTask({ + image: container.image || '', + command: container.command || [], + }); + + return newJobName; + } +} diff --git a/api-backend/src/ovh/processors/ovh-task.processor.ts b/api-backend/src/ovh/processors/ovh-task.processor.ts new file mode 100644 index 0000000..36904e5 --- /dev/null +++ b/api-backend/src/ovh/processors/ovh-task.processor.ts @@ -0,0 +1,67 @@ +import { Logger } from '@nestjs/common'; + +import { OvhService } from '../ovh.service'; + +import { + OVH_TASK_INITIATE_FAILED_TASK, + OVH_TASK_QUEUE, +} from 'src/constants/bull-queue'; +import { Bot, ExecutionStatusLogEnum } from 'src/database/models/bot.model'; +import { InjectModel } from '@nestjs/sequelize'; +import { ConfigService } from '@nestjs/config'; +import { OnWorkerEvent, Processor, WorkerHost } from '@nestjs/bullmq'; +import { Job } from 'bullmq'; + +@Processor(OVH_TASK_QUEUE) +export class OVHTaskProcessor extends WorkerHost { + constructor( + private readonly ovhService: OvhService, + @InjectModel(Bot) + private readonly botModel: typeof Bot, + private readonly configService: ConfigService, + ) { + super(); + } + public readonly logger = new Logger(OVHTaskProcessor.name); + + process(job: Job): Promise { + const { name, data } = job; + + switch (name) { + case OVH_TASK_INITIATE_FAILED_TASK: + return this.reInitiateFailedTasks(data.botId); + } + } + + async reInitiateFailedTasks(botId: string): Promise { + if (!botId) return; + + const bot = await this.botModel.findByPk(botId); + + if ( + bot.taskId && + bot.status === ExecutionStatusLogEnum.FAILED && + bot.retryCount < this.configService.get('bot.meetingBotRetryCount') + ) { + const newTaskId = await this.ovhService.reInitiateTask(bot.taskId); + + await bot.update({ + taskId: newTaskId, + status: ExecutionStatusLogEnum.STARTED, + retryCount: bot.retryCount + 1, + }); + } + } + + @OnWorkerEvent('completed') + onCompleted(job: Job): void { + this.logger.log(`OVH TASK QUEUE: Job ${job.id} completed successfully`); + } + + @OnWorkerEvent('failed') + onFailed(job: Job, error: Error): void { + this.logger.error( + `OVH TASK QUEUE: Job ${job.id} failed with error: ${error.message}`, + ); + } +} diff --git a/api-backend/yarn.lock b/api-backend/yarn.lock index 49a4a33..609b478 100644 --- a/api-backend/yarn.lock +++ b/api-backend/yarn.lock @@ -1300,6 +1300,38 @@ resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== +"@jsep-plugin/assignment@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@jsep-plugin/assignment/-/assignment-1.3.0.tgz#fcfc5417a04933f7ceee786e8ab498aa3ce2b242" + integrity sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ== + +"@jsep-plugin/regex@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@jsep-plugin/regex/-/regex-1.0.4.tgz#cb2fc423220fa71c609323b9ba7f7d344a755fcc" + integrity sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg== + +"@kubernetes/client-node@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@kubernetes/client-node/-/client-node-1.3.0.tgz#6088bc1c4f6bb01f52e6e933f35008feb39fe909" + integrity sha512-IE0yrIpOT97YS5fg2QpzmPzm8Wmcdf4ueWMn+FiJSI3jgTTQT1u+LUhoYpdfhdHAVxdrNsaBg2C0UXSnOgMoCQ== + dependencies: + "@types/js-yaml" "^4.0.1" + "@types/node" "^22.0.0" + "@types/node-fetch" "^2.6.9" + "@types/stream-buffers" "^3.0.3" + form-data "^4.0.0" + hpagent "^1.2.0" + isomorphic-ws "^5.0.0" + js-yaml "^4.1.0" + jsonpath-plus "^10.3.0" + node-fetch "^2.6.9" + openid-client "^6.1.3" + rfc4648 "^1.3.0" + socks-proxy-agent "^8.0.4" + stream-buffers "^3.0.2" + tar-fs "^3.0.8" + ws "^8.18.2" + "@ljharb/through@^2.3.12": version "2.3.14" resolved "https://registry.yarnpkg.com/@ljharb/through/-/through-2.3.14.tgz#a5df44295f44dc23bfe106af59426dd0677760b1" @@ -2240,6 +2272,11 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/js-yaml@^4.0.1": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" + integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== + "@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -2270,6 +2307,14 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== +"@types/node-fetch@^2.6.9": + version "2.6.12" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03" + integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node@*": version "22.13.9" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.9.tgz#5d9a8f7a975a5bd3ef267352deb96fb13ec02eca" @@ -2291,6 +2336,13 @@ dependencies: undici-types "~6.19.2" +"@types/node@^22.0.0": + version "22.15.32" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.32.tgz#c301cc2275b535a5e54bb81d516b1d2e9afe06e5" + integrity sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA== + dependencies: + undici-types "~6.21.0" + "@types/qs@*": version "6.9.18" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.18.tgz#877292caa91f7c1b213032b34626505b746624c2" @@ -2338,6 +2390,13 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/stream-buffers@^3.0.3": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/stream-buffers/-/stream-buffers-3.0.7.tgz#0b719fa1bd2ca2cc0908205a440e5e569e1aa21e" + integrity sha512-azOCy05sXVXrO+qklf0c/B07H/oHaIuDDAiHPVwlk3A9Ek+ksHyTeMajLZl3r76FxpPpxem//4Te61G1iW3Giw== + dependencies: + "@types/node" "*" + "@types/superagent@^8.1.0": version "8.1.9" resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.9.tgz#28bfe4658e469838ed0bf66d898354bcab21f49f" @@ -2625,6 +2684,11 @@ acorn@^8.11.0, acorn@^8.14.0, acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== +agent-base@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" + integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== + ajv-formats@2.1.1, ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2773,6 +2837,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +b4a@^1.6.4: + version "1.6.7" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" + integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== + babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" @@ -2841,6 +2910,39 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bare-events@^2.2.0, bare-events@^2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.4.tgz#16143d435e1ed9eafd1ab85f12b89b3357a41745" + integrity sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA== + +bare-fs@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-4.1.5.tgz#1d06c076e68cc8bf97010d29af9e3ac3808cdcf7" + integrity sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA== + dependencies: + bare-events "^2.5.4" + bare-path "^3.0.0" + bare-stream "^2.6.4" + +bare-os@^3.0.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-3.6.1.tgz#9921f6f59edbe81afa9f56910658422c0f4858d4" + integrity sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g== + +bare-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-3.0.0.tgz#b59d18130ba52a6af9276db3e96a2e3d3ea52178" + integrity sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw== + dependencies: + bare-os "^3.0.1" + +bare-stream@^2.6.4: + version "2.6.5" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.6.5.tgz#bba8e879674c4c27f7e27805df005c15d7a2ca07" + integrity sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA== + dependencies: + streamx "^2.21.0" + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -3494,6 +3596,13 @@ encodeurl@~2.0.0: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== +end-of-stream@^1.1.0: + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== + dependencies: + once "^1.4.0" + enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0: version "5.18.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" @@ -3784,6 +3893,11 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== +fast-fifo@^1.2.0, fast-fifo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + fast-glob@^3.2.9: version "3.3.3" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" @@ -4176,6 +4290,11 @@ hexoid@^1.0.0: resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== +hpagent@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-1.2.0.tgz#0ae417895430eb3770c03443456b8d90ca464903" + integrity sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -4310,6 +4429,14 @@ ioredis@^5.4.1: redis-parser "^3.0.0" standard-as-callback "^2.1.0" +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -4391,6 +4518,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isomorphic-ws@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== + istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" @@ -4835,6 +4967,11 @@ jest@^29.5.0: import-local "^3.0.2" jest-cli "^29.7.0" +jose@^6.0.11: + version "6.0.11" + resolved "https://registry.yarnpkg.com/jose/-/jose-6.0.11.tgz#0b7ea8b3b21a1bda5e00255a044c3a0e43270882" + integrity sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4855,6 +4992,16 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +jsep@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jsep/-/jsep-1.4.0.tgz#19feccbfa51d8a79f72480b4b8e40ce2e17152f0" + integrity sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw== + jsesc@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" @@ -4909,6 +5056,15 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonpath-plus@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz#59e22e4fa2298c68dfcd70659bb47f0cad525238" + integrity sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA== + dependencies: + "@jsep-plugin/assignment" "^1.3.0" + "@jsep-plugin/regex" "^1.0.4" + jsep "^1.4.0" + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -5259,7 +5415,7 @@ node-emoji@1.11.0: dependencies: lodash "^4.17.21" -node-fetch@^2.6.1: +node-fetch@^2.6.1, node-fetch@^2.6.9: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -5295,6 +5451,11 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +oauth4webapi@^3.5.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/oauth4webapi/-/oauth4webapi-3.5.3.tgz#91fbb6da50fae4f33073b4dc1900396a41a18d57" + integrity sha512-2bnHosmBLAQpXNBLOvaJMyMkr4Yya5ohE5Q9jqyxiN+aa7GFCzvDN1RRRMrp0NkfqRR2MTaQNkcSUCCjILD9oQ== + object-assign@^4, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -5312,7 +5473,7 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -5326,6 +5487,14 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +openid-client@^6.1.3: + version "6.5.1" + resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-6.5.1.tgz#adcef1b0c2d6f1128aa010d4c2406f9e91d18270" + integrity sha512-DNq7s+Tm9wfMUTltl+kUJzwi+bsbeiZycDm1gJQbX6MTHwo1+Q0I3F+ccsxi1T3mYMaHATCSnWDridkZ3hnu1g== + dependencies: + jose "^6.0.11" + oauth4webapi "^3.5.2" + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -5636,6 +5805,14 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +pump@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d" + integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -5802,6 +5979,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== +rfc4648@^1.3.0: + version "1.5.4" + resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.5.4.tgz#1174c0afba72423a0b70c386ecfeb80aa61b05ca" + integrity sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -6043,6 +6225,28 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^8.0.4: + version "8.0.5" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee" + integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw== + dependencies: + agent-base "^7.1.2" + debug "^4.3.4" + socks "^2.8.3" + +socks@^2.8.3: + version "2.8.5" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.5.tgz#bfe18f5ead1efc93f5ec90c79fa8bdccbcee2e64" + integrity sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -6074,6 +6278,11 @@ split2@^4.1.0: resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -6096,11 +6305,26 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +stream-buffers@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.3.tgz#9fc6ae267d9c4df1190a781e011634cac58af3cd" + integrity sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw== + streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +streamx@^2.15.0, streamx@^2.21.0: + version "2.22.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.22.1.tgz#c97cbb0ce18da4f4db5a971dc9ab68ff5dc7f5a5" + integrity sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA== + dependencies: + fast-fifo "^1.3.2" + text-decoder "^1.1.0" + optionalDependencies: + bare-events "^2.2.0" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -6241,6 +6465,26 @@ tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tar-fs@^3.0.8: + version "3.0.10" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.10.tgz#60f8ccd60fe30164bdd3d6606619650236ed38f7" + integrity sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA== + dependencies: + pump "^3.0.0" + tar-stream "^3.1.5" + optionalDependencies: + bare-fs "^4.0.1" + bare-path "^3.0.0" + +tar-stream@^3.1.5: + version "3.1.7" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" + integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + terser-webpack-plugin@^5.3.10: version "5.3.12" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.12.tgz#d9518c80493081bace668aa8613b22e4a838810c" @@ -6271,6 +6515,13 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-decoder@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65" + integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA== + dependencies: + b4a "^1.6.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -6487,6 +6738,11 @@ undici-types@~6.20.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" @@ -6695,6 +6951,11 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@^8.18.2: + version "8.18.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" + integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"