add tailscale node delete!
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-08-07 10:25:49 +09:00
parent 8151ec094d
commit 8b7ec648cd
3 changed files with 92 additions and 4 deletions
+67 -2
View File
@@ -1736,13 +1736,33 @@ export class AppsService {
async deleteRuntimeProfile(profileId: string) { async deleteRuntimeProfile(profileId: string) {
const profile = await this.prisma.runtimeEnvProfile.findUnique({ const profile = await this.prisma.runtimeEnvProfile.findUnique({
where: { id: profileId }, where: { id: profileId },
select: { id: true, name: true }, include: {
variables: {
include: {
component: {
select: {
name: true,
type: true,
workloadType: true,
},
},
},
},
},
}); });
if (!profile) { if (!profile) {
throw new NotFoundException('실행 설정 프로필을 찾을 수 없습니다.'); throw new NotFoundException('실행 설정 프로필을 찾을 수 없습니다.');
} }
for (const variable of profile.variables) {
await this.removeRuntimeVariableFromWorkload(
normalizeNamespace(profile.namespace),
variable.component,
variable.key,
);
}
await this.prisma.runtimeEnvProfile.delete({ await this.prisma.runtimeEnvProfile.delete({
where: { id: profileId }, where: { id: profileId },
}); });
@@ -1845,13 +1865,32 @@ export class AppsService {
async deleteRuntimeVariable(profileId: string, variableId: string) { async deleteRuntimeVariable(profileId: string, variableId: string) {
const variable = await this.prisma.runtimeEnvVariable.findFirst({ const variable = await this.prisma.runtimeEnvVariable.findFirst({
where: { id: variableId, profileId }, where: { id: variableId, profileId },
select: { id: true }, include: {
profile: {
select: {
namespace: true,
},
},
component: {
select: {
name: true,
type: true,
workloadType: true,
},
},
},
}); });
if (!variable) { if (!variable) {
throw new NotFoundException('환경변수를 찾을 수 없습니다.'); throw new NotFoundException('환경변수를 찾을 수 없습니다.');
} }
await this.removeRuntimeVariableFromWorkload(
normalizeNamespace(variable.profile.namespace),
variable.component,
variable.key,
);
await this.prisma.runtimeEnvVariable.delete({ await this.prisma.runtimeEnvVariable.delete({
where: { id: variable.id }, 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() { private async kubectlEnv() {
const inClusterHost = process.env.KUBERNETES_SERVICE_HOST; const inClusterHost = process.env.KUBERNETES_SERVICE_HOST;
if (inClusterHost) { if (inClusterHost) {
+6 -1
View File
@@ -1,4 +1,4 @@
import { Controller, Get } from '@nestjs/common'; import { Controller, Delete, Get, Param } from '@nestjs/common';
import { TailscaleService } from './tailscale.service'; import { TailscaleService } from './tailscale.service';
@@ -11,6 +11,11 @@ export class TailscaleController {
return this.tailscaleService.listNodes(); return this.tailscaleService.listNodes();
} }
@Delete('nodes/:nodeId')
deleteNode(@Param('nodeId') nodeId: string) {
return this.tailscaleService.deleteNode(nodeId);
}
@Get('routes') @Get('routes')
listRoutes() { listRoutes() {
return this.tailscaleService.listRoutes(); return this.tailscaleService.listRoutes();
+19 -1
View File
@@ -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() { async listRoutes() {
if (!this.gatewayUrl || !this.gatewayToken) { if (!this.gatewayUrl || !this.gatewayToken) {
return { return {
@@ -118,11 +134,13 @@ export class TailscaleService {
} }
} }
private async request<T>(path: string): Promise<T> { private async request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${this.gatewayUrl}${path}`, { const response = await fetch(`${this.gatewayUrl}${path}`, {
...init,
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'x-platform-gateway-token': this.gatewayToken ?? '', 'x-platform-gateway-token': this.gatewayToken ?? '',
...(init?.headers ?? {}),
}, },
}); });