diff --git a/src/apps/apps.controller.ts b/src/apps/apps.controller.ts index 40d647b..9bd636b 100644 --- a/src/apps/apps.controller.ts +++ b/src/apps/apps.controller.ts @@ -195,6 +195,15 @@ export class AppsController { return this.appsService.listRuntimeResources(namespace); } + @Get('runtime-namespaces/:namespace/resources/:kind/:name/yaml') + getRuntimeResourceYaml( + @Param('namespace') namespace: string, + @Param('kind') kind: string, + @Param('name') name: string, + ) { + return this.appsService.getRuntimeResourceYaml(namespace, kind, name); + } + @Post('runtime-namespaces') ensureRuntimeNamespace(@Body() body: EnsureRuntimeNamespaceDto) { return this.appsService.ensureRuntimeNamespace(body); diff --git a/src/apps/apps.service.ts b/src/apps/apps.service.ts index 1b64f07..7c7b1d4 100644 --- a/src/apps/apps.service.ts +++ b/src/apps/apps.service.ts @@ -1506,7 +1506,7 @@ export class AppsService { { kind: 'StatefulSet', command: 'statefulsets' }, { kind: 'Service', command: 'services' }, { kind: 'Ingress', command: 'ingress' }, - { kind: 'PVC', command: 'pvc' }, + { kind: 'PersistentVolumeClaim', command: 'pvc' }, ]; const resources = []; @@ -1530,12 +1530,32 @@ export class AppsService { deployments: resources.filter((resource) => resource.kind === 'Deployment').length, statefulSets: resources.filter((resource) => resource.kind === 'StatefulSet').length, services: resources.filter((resource) => resource.kind === 'Service').length, - persistentVolumeClaims: resources.filter((resource) => resource.kind === 'PVC').length, + persistentVolumeClaims: resources.filter((resource) => resource.kind === 'PersistentVolumeClaim').length, }, resources, }; } + async getRuntimeResourceYaml(namespace: string, kind: string, name: string) { + const normalizedNamespace = normalizeNamespace(namespace); + const resourceType = getKubectlResourceType(kind); + if (!resourceType) { + throw new BadRequestException('YAML 조회를 지원하지 않는 리소스 유형입니다.'); + } + + const yaml = await this.kubectl([ + 'get', + resourceType, + name, + '-n', + normalizedNamespace, + '-o', + 'yaml', + ]); + + return { namespace: normalizedNamespace, kind, name, yaml }; + } + async ensureRuntimeNamespace(body: EnsureRuntimeNamespaceDto) { const namespace = normalizeNamespace(body.namespace); const logs: string[] = []; @@ -2754,6 +2774,26 @@ function getNumber(value: unknown, fallback = 0) { return typeof value === 'number' ? value : fallback; } +function getKubectlResourceType(kind: string) { + const resourceTypes: Record = { + Deployment: 'deployment', + StatefulSet: 'statefulset', + DaemonSet: 'daemonset', + Pod: 'pod', + Job: 'job', + CronJob: 'cronjob', + ReplicaSet: 'replicaset', + Service: 'service', + Ingress: 'ingress', + PersistentVolumeClaim: 'pvc', + PVC: 'pvc', + Endpoint: 'endpoints', + Endpoints: 'endpoints', + }; + + return resourceTypes[kind]; +} + function getAge(createdAt: string) { const created = new Date(createdAt).getTime(); if (!created || Number.isNaN(created)) return '-'; diff --git a/src/licenses/licenses.controller.ts b/src/licenses/licenses.controller.ts index 0c269de..fe8c394 100644 --- a/src/licenses/licenses.controller.ts +++ b/src/licenses/licenses.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req } from '@nestjs/common'; import { AssignLicenseDto, ListUserLicensesQuery, UpdateLicenseAppPermissionsDto, UpdateLicensePlanDto } from './dto'; import { LicensesService } from './licenses.service'; @@ -38,13 +38,21 @@ export class LicensesController { } @Post('users/:userId') - assign(@Param('userId') userId: string, @Body() body: AssignLicenseDto) { - return this.licensesService.assign(userId, body); + assign( + @Param('userId') userId: string, + @Body() body: AssignLicenseDto, + @Req() request: { admin?: { id: string } }, + ) { + return this.licensesService.assign(userId, body, request.admin?.id); } @Patch('user-licenses/:id') - update(@Param('id') id: string, @Body() body: AssignLicenseDto) { - return this.licensesService.update(id, body); + update( + @Param('id') id: string, + @Body() body: AssignLicenseDto, + @Req() request: { admin?: { id: string } }, + ) { + return this.licensesService.update(id, body, request.admin?.id); } @Delete('user-licenses/:id') diff --git a/src/licenses/licenses.module.ts b/src/licenses/licenses.module.ts index 2b3e2a2..7fb8b87 100644 --- a/src/licenses/licenses.module.ts +++ b/src/licenses/licenses.module.ts @@ -2,11 +2,12 @@ import { Module } from '@nestjs/common'; import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module'; import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module'; +import { PrismaModule } from '../prisma/prisma.module'; import { LicensesController } from './licenses.controller'; import { LicensesService } from './licenses.service'; @Module({ - imports: [AuthPrismaModule, LauncherPrismaModule], + imports: [AuthPrismaModule, LauncherPrismaModule, PrismaModule], controllers: [LicensesController], providers: [LicensesService], }) diff --git a/src/licenses/licenses.service.ts b/src/licenses/licenses.service.ts index 361ad52..3e4c68d 100644 --- a/src/licenses/licenses.service.ts +++ b/src/licenses/licenses.service.ts @@ -2,6 +2,7 @@ import { BadRequestException, ConflictException, Injectable, NotFoundException } import { AuthPrismaService } from '../auth-prisma/auth-prisma.service'; import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service'; +import { PrismaService } from '../prisma/prisma.service'; import { AssignLicenseDto, ListUserLicensesQuery, UpdateLicenseAppPermissionsDto, UpdateLicensePlanDto } from './dto'; type LauncherAppPermissionRow = { @@ -18,6 +19,7 @@ export class LicensesService { constructor( private readonly authPrisma: AuthPrismaService, private readonly launcherPrisma: LauncherPrismaService, + private readonly prisma: PrismaService, ) {} async listPlans() { @@ -141,6 +143,15 @@ export class LicensesService { this.authPrisma.userLicense.count({ where }), ]); + const assignedByIds = [...new Set(items.flatMap((item) => item.assignedById ? [item.assignedById] : []))]; + const admins = assignedByIds.length + ? await this.prisma.admin.findMany({ + where: { id: { in: assignedByIds } }, + select: { id: true, name: true }, + }) + : []; + const assignedByName = new Map(admins.map((admin) => [admin.id, admin.name])); + return { assignments: items.map((userLicense, index) => ({ id: userLicense.id, @@ -153,7 +164,9 @@ export class LicensesService { licenseName: userLicense.license.name, startsAt: userLicense.startsAt, expiresAt: userLicense.expiresAt, - assignedBy: userLicense.assignedById, + assignedBy: userLicense.assignedById + ? assignedByName.get(userLicense.assignedById) ?? '-' + : '-', status: isActiveLicense(userLicense.startsAt, userLicense.expiresAt, now) ? '활성' : '만료', })), total, @@ -279,7 +292,7 @@ export class LicensesService { return userIds.length; } - async assign(userId: string, body: AssignLicenseDto) { + async assign(userId: string, body: AssignLicenseDto, assignedById?: string) { const user = await this.authPrisma.user.findUnique({ where: { id: userId }, select: { id: true }, @@ -321,7 +334,7 @@ export class LicensesService { licenseId: license.id, startsAt, expiresAt, - assignedById: body.assignedById, + assignedById, }, include: { license: true, user: true }, }); @@ -331,7 +344,7 @@ export class LicensesService { return { userLicense }; } - async update(id: string, body: AssignLicenseDto) { + async update(id: string, body: AssignLicenseDto, assignedById?: string) { const current = await this.authPrisma.userLicense.findUnique({ where: { id }, select: { id: true }, @@ -360,7 +373,7 @@ export class LicensesService { licenseId: license.id, startsAt, expiresAt, - assignedById: body.assignedById, + assignedById, }, include: { license: true, user: true }, });