add infrastructure resource YAML viewer and fix PVC visibility
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-14 13:35:59 +09:00
parent 78f3578098
commit d922b873e4
5 changed files with 84 additions and 13 deletions
+9
View File
@@ -195,6 +195,15 @@ export class AppsController {
return this.appsService.listRuntimeResources(namespace); 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') @Post('runtime-namespaces')
ensureRuntimeNamespace(@Body() body: EnsureRuntimeNamespaceDto) { ensureRuntimeNamespace(@Body() body: EnsureRuntimeNamespaceDto) {
return this.appsService.ensureRuntimeNamespace(body); return this.appsService.ensureRuntimeNamespace(body);
+42 -2
View File
@@ -1506,7 +1506,7 @@ export class AppsService {
{ kind: 'StatefulSet', command: 'statefulsets' }, { kind: 'StatefulSet', command: 'statefulsets' },
{ kind: 'Service', command: 'services' }, { kind: 'Service', command: 'services' },
{ kind: 'Ingress', command: 'ingress' }, { kind: 'Ingress', command: 'ingress' },
{ kind: 'PVC', command: 'pvc' }, { kind: 'PersistentVolumeClaim', command: 'pvc' },
]; ];
const resources = []; const resources = [];
@@ -1530,12 +1530,32 @@ export class AppsService {
deployments: resources.filter((resource) => resource.kind === 'Deployment').length, deployments: resources.filter((resource) => resource.kind === 'Deployment').length,
statefulSets: resources.filter((resource) => resource.kind === 'StatefulSet').length, statefulSets: resources.filter((resource) => resource.kind === 'StatefulSet').length,
services: resources.filter((resource) => resource.kind === 'Service').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, 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) { async ensureRuntimeNamespace(body: EnsureRuntimeNamespaceDto) {
const namespace = normalizeNamespace(body.namespace); const namespace = normalizeNamespace(body.namespace);
const logs: string[] = []; const logs: string[] = [];
@@ -2754,6 +2774,26 @@ function getNumber(value: unknown, fallback = 0) {
return typeof value === 'number' ? value : fallback; return typeof value === 'number' ? value : fallback;
} }
function getKubectlResourceType(kind: string) {
const resourceTypes: Record<string, string> = {
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) { function getAge(createdAt: string) {
const created = new Date(createdAt).getTime(); const created = new Date(createdAt).getTime();
if (!created || Number.isNaN(created)) return '-'; if (!created || Number.isNaN(created)) return '-';
+13 -5
View File
@@ -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 { AssignLicenseDto, ListUserLicensesQuery, UpdateLicenseAppPermissionsDto, UpdateLicensePlanDto } from './dto';
import { LicensesService } from './licenses.service'; import { LicensesService } from './licenses.service';
@@ -38,13 +38,21 @@ export class LicensesController {
} }
@Post('users/:userId') @Post('users/:userId')
assign(@Param('userId') userId: string, @Body() body: AssignLicenseDto) { assign(
return this.licensesService.assign(userId, body); @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') @Patch('user-licenses/:id')
update(@Param('id') id: string, @Body() body: AssignLicenseDto) { update(
return this.licensesService.update(id, body); @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') @Delete('user-licenses/:id')
+2 -1
View File
@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module'; import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module';
import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module'; import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module';
import { PrismaModule } from '../prisma/prisma.module';
import { LicensesController } from './licenses.controller'; import { LicensesController } from './licenses.controller';
import { LicensesService } from './licenses.service'; import { LicensesService } from './licenses.service';
@Module({ @Module({
imports: [AuthPrismaModule, LauncherPrismaModule], imports: [AuthPrismaModule, LauncherPrismaModule, PrismaModule],
controllers: [LicensesController], controllers: [LicensesController],
providers: [LicensesService], providers: [LicensesService],
}) })
+18 -5
View File
@@ -2,6 +2,7 @@ import { BadRequestException, ConflictException, Injectable, NotFoundException }
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service'; import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service'; import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service';
import { PrismaService } from '../prisma/prisma.service';
import { AssignLicenseDto, ListUserLicensesQuery, UpdateLicenseAppPermissionsDto, UpdateLicensePlanDto } from './dto'; import { AssignLicenseDto, ListUserLicensesQuery, UpdateLicenseAppPermissionsDto, UpdateLicensePlanDto } from './dto';
type LauncherAppPermissionRow = { type LauncherAppPermissionRow = {
@@ -18,6 +19,7 @@ export class LicensesService {
constructor( constructor(
private readonly authPrisma: AuthPrismaService, private readonly authPrisma: AuthPrismaService,
private readonly launcherPrisma: LauncherPrismaService, private readonly launcherPrisma: LauncherPrismaService,
private readonly prisma: PrismaService,
) {} ) {}
async listPlans() { async listPlans() {
@@ -141,6 +143,15 @@ export class LicensesService {
this.authPrisma.userLicense.count({ where }), 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 { return {
assignments: items.map((userLicense, index) => ({ assignments: items.map((userLicense, index) => ({
id: userLicense.id, id: userLicense.id,
@@ -153,7 +164,9 @@ export class LicensesService {
licenseName: userLicense.license.name, licenseName: userLicense.license.name,
startsAt: userLicense.startsAt, startsAt: userLicense.startsAt,
expiresAt: userLicense.expiresAt, expiresAt: userLicense.expiresAt,
assignedBy: userLicense.assignedById, assignedBy: userLicense.assignedById
? assignedByName.get(userLicense.assignedById) ?? '-'
: '-',
status: isActiveLicense(userLicense.startsAt, userLicense.expiresAt, now) ? '활성' : '만료', status: isActiveLicense(userLicense.startsAt, userLicense.expiresAt, now) ? '활성' : '만료',
})), })),
total, total,
@@ -279,7 +292,7 @@ export class LicensesService {
return userIds.length; return userIds.length;
} }
async assign(userId: string, body: AssignLicenseDto) { async assign(userId: string, body: AssignLicenseDto, assignedById?: string) {
const user = await this.authPrisma.user.findUnique({ const user = await this.authPrisma.user.findUnique({
where: { id: userId }, where: { id: userId },
select: { id: true }, select: { id: true },
@@ -321,7 +334,7 @@ export class LicensesService {
licenseId: license.id, licenseId: license.id,
startsAt, startsAt,
expiresAt, expiresAt,
assignedById: body.assignedById, assignedById,
}, },
include: { license: true, user: true }, include: { license: true, user: true },
}); });
@@ -331,7 +344,7 @@ export class LicensesService {
return { userLicense }; return { userLicense };
} }
async update(id: string, body: AssignLicenseDto) { async update(id: string, body: AssignLicenseDto, assignedById?: string) {
const current = await this.authPrisma.userLicense.findUnique({ const current = await this.authPrisma.userLicense.findUnique({
where: { id }, where: { id },
select: { id: true }, select: { id: true },
@@ -360,7 +373,7 @@ export class LicensesService {
licenseId: license.id, licenseId: license.id,
startsAt, startsAt,
expiresAt, expiresAt,
assignedById: body.assignedById, assignedById,
}, },
include: { license: true, user: true }, include: { license: true, user: true },
}); });