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);
}
@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);
+42 -2
View File
@@ -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<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) {
const created = new Date(createdAt).getTime();
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 { 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')
+2 -1
View File
@@ -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],
})
+18 -5
View File
@@ -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 },
});