From 8b7ec648cd1afe68d1a0ba19dfd62978e822f44d Mon Sep 17 00:00:00 2001 From: chyoo Date: Fri, 7 Aug 2026 10:25:49 +0900 Subject: [PATCH] add tailscale node delete! --- src/apps/apps.service.ts | 69 ++++++++++++++++++++++++++- src/tailscale/tailscale.controller.ts | 7 ++- src/tailscale/tailscale.service.ts | 20 +++++++- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/apps/apps.service.ts b/src/apps/apps.service.ts index ebde14b..327ef0b 100644 --- a/src/apps/apps.service.ts +++ b/src/apps/apps.service.ts @@ -1736,13 +1736,33 @@ export class AppsService { async deleteRuntimeProfile(profileId: string) { const profile = await this.prisma.runtimeEnvProfile.findUnique({ where: { id: profileId }, - select: { id: true, name: true }, + include: { + variables: { + include: { + component: { + select: { + name: true, + type: true, + workloadType: true, + }, + }, + }, + }, + }, }); if (!profile) { throw new NotFoundException('실행 설정 프로필을 찾을 수 없습니다.'); } + for (const variable of profile.variables) { + await this.removeRuntimeVariableFromWorkload( + normalizeNamespace(profile.namespace), + variable.component, + variable.key, + ); + } + await this.prisma.runtimeEnvProfile.delete({ where: { id: profileId }, }); @@ -1845,13 +1865,32 @@ export class AppsService { async deleteRuntimeVariable(profileId: string, variableId: string) { const variable = await this.prisma.runtimeEnvVariable.findFirst({ where: { id: variableId, profileId }, - select: { id: true }, + include: { + profile: { + select: { + namespace: true, + }, + }, + component: { + select: { + name: true, + type: true, + workloadType: true, + }, + }, + }, }); if (!variable) { throw new NotFoundException('환경변수를 찾을 수 없습니다.'); } + await this.removeRuntimeVariableFromWorkload( + normalizeNamespace(variable.profile.namespace), + variable.component, + variable.key, + ); + await this.prisma.runtimeEnvVariable.delete({ where: { id: variable.id }, }); @@ -2205,6 +2244,32 @@ export class AppsService { } } + private async removeRuntimeVariableFromWorkload( + namespace: string, + component: { name: string; type: string; workloadType: string | null } | null, + key: string, + ) { + if (!component || component.type === 'DATABASE') { + return; + } + + const workloadType = normalizeWorkloadType(component.workloadType, component.type); + if (workloadType === 'Job') { + return; + } + + const workloadRef = `${workloadType.toLowerCase()}/${sanitizeK8sName(component.name)}`; + try { + await this.kubectl(['set', 'env', workloadRef, `${key}-`, '-n', namespace]); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (/\bnot\s*found\b/i.test(message)) { + return; + } + throw error; + } + } + private async kubectlEnv() { const inClusterHost = process.env.KUBERNETES_SERVICE_HOST; if (inClusterHost) { diff --git a/src/tailscale/tailscale.controller.ts b/src/tailscale/tailscale.controller.ts index ead04fe..9d4ab84 100644 --- a/src/tailscale/tailscale.controller.ts +++ b/src/tailscale/tailscale.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Delete, Get, Param } from '@nestjs/common'; import { TailscaleService } from './tailscale.service'; @@ -11,6 +11,11 @@ export class TailscaleController { return this.tailscaleService.listNodes(); } + @Delete('nodes/:nodeId') + deleteNode(@Param('nodeId') nodeId: string) { + return this.tailscaleService.deleteNode(nodeId); + } + @Get('routes') listRoutes() { return this.tailscaleService.listRoutes(); diff --git a/src/tailscale/tailscale.service.ts b/src/tailscale/tailscale.service.ts index d33fb35..6b60dec 100644 --- a/src/tailscale/tailscale.service.ts +++ b/src/tailscale/tailscale.service.ts @@ -63,6 +63,22 @@ export class TailscaleService { } } + async deleteNode(nodeId: string) { + const normalizedNodeId = nodeId.trim(); + if (!normalizedNodeId) { + throw new BadGatewayException('삭제할 Headscale 노드 ID가 없습니다.'); + } + if (!this.gatewayUrl || !this.gatewayToken) { + throw new BadGatewayException('PLATFORM_GATEWAY_URL 또는 PLATFORM_GATEWAY_TOKEN이 설정되어 있지 않습니다.'); + } + + await this.request<{ deleted?: boolean }>(`/api/admin/tailscale/nodes/${encodeURIComponent(normalizedNodeId)}`, { + method: 'DELETE', + }); + + return { deleted: true, nodeId: normalizedNodeId }; + } + async listRoutes() { if (!this.gatewayUrl || !this.gatewayToken) { return { @@ -118,11 +134,13 @@ export class TailscaleService { } } - private async request(path: string): Promise { + private async request(path: string, init?: RequestInit): Promise { const response = await fetch(`${this.gatewayUrl}${path}`, { + ...init, headers: { Accept: 'application/json', 'x-platform-gateway-token': this.gatewayToken ?? '', + ...(init?.headers ?? {}), }, });