add infrastructure resource YAML viewer and fix PVC visibility
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 '-';
|
||||||
|
|||||||
@@ -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,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],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user