add build env and path ingress controls

This commit is contained in:
2026-07-02 15:44:12 +09:00
parent 3195d36527
commit 2729e95a5b
7 changed files with 226 additions and 4 deletions
+118 -1
View File
@@ -22,6 +22,7 @@ import {
CreateRuntimeEnvVariableDto,
CreateRuntimeIngressRuleDto,
EnsureRuntimeNamespaceDto,
ReplaceBuildEnvVariablesDto,
} from './dto';
const execFileAsync = promisify(execFile);
@@ -43,6 +44,9 @@ export class AppsService {
components: {
orderBy: { name: 'asc' },
include: {
buildVariables: {
orderBy: { key: 'asc' },
},
buildJobs: {
where: { status: 'SUCCEEDED' },
orderBy: { createdAt: 'desc' },
@@ -194,6 +198,56 @@ export class AppsService {
return { component };
}
async listComponentBuildEnv(componentId: string) {
const component = await this.prisma.appComponent.findUnique({
where: { id: componentId },
include: {
buildVariables: {
orderBy: { key: 'asc' },
},
},
});
if (!component) {
throw new NotFoundException('앱 구성요소를 찾을 수 없습니다.');
}
return { variables: component.buildVariables };
}
async replaceComponentBuildEnv(
componentId: string,
body: ReplaceBuildEnvVariablesDto,
) {
const component = await this.prisma.appComponent.findUnique({
where: { id: componentId },
});
if (!component) {
throw new NotFoundException('앱 구성요소를 찾을 수 없습니다.');
}
const variables = normalizeBuildEnvVariables(body.variables ?? []);
await this.prisma.$transaction([
this.prisma.buildEnvVariable.deleteMany({ where: { componentId } }),
...(variables.length
? [
this.prisma.buildEnvVariable.createMany({
data: variables.map((variable) => ({
componentId,
key: variable.key,
value: variable.value,
isSecret: variable.isSecret,
})),
}),
]
: []),
]);
return this.listComponentBuildEnv(componentId);
}
listReleases(appKey?: string) {
return this.prisma.appRelease.findMany({
where: appKey ? { app: { key: appKey } } : undefined,
@@ -415,7 +469,21 @@ export class AppsService {
if (runtimeProfile?.ingressRules.length) {
for (const rule of runtimeProfile.ingressRules) {
const serviceName = sanitizeK8sName(rule.component.name);
const ingressName = sanitizeK8sName(`${rule.component.name}-${rule.host}`);
const ingressName = sanitizeK8sName(`${rule.component.name}-${rule.host}-${rule.path}`);
const middlewareName = rule.stripPrefix ? sanitizeK8sName(`${ingressName}-strip`) : null;
if (middlewareName) {
logs.push(`[apply] middleware/${middlewareName}`);
appendOutput(
logs,
await this.applyK8sManifest(createTraefikStripPrefixMiddlewareManifest({
namespace,
name: middlewareName,
prefix: rule.path,
})),
);
}
logs.push(`[apply] ingress/${ingressName}`);
appendOutput(
logs,
@@ -426,6 +494,7 @@ export class AppsService {
path: rule.path,
serviceName,
servicePort: rule.servicePort,
middlewareName,
tlsEnabled: rule.tlsEnabled,
tlsSecretName: rule.tlsSecretName,
})),
@@ -875,6 +944,7 @@ export class AppsService {
host,
path: pathValue,
servicePort: body.servicePort,
stripPrefix: Boolean(body.stripPrefix),
tlsEnabled: Boolean(body.tlsEnabled),
tlsSecretName: body.tlsSecretName?.trim() || null,
},
@@ -1367,6 +1437,24 @@ function normalizeOptionalString(value?: string | null) {
return trimmed || undefined;
}
function normalizeBuildEnvVariables(
variables: Array<{ key?: string; value?: string; isSecret?: boolean }>,
) {
const byKey = new Map<string, { key: string; value: string; isSecret: boolean }>();
for (const variable of variables) {
const key = variable.key?.trim();
if (!key) continue;
byKey.set(key, {
key,
value: variable.value ?? '',
isSecret: variable.isSecret ?? false,
});
}
return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
}
function normalizeImageSource(value: string | null | undefined, componentType?: string) {
const normalized = value?.trim().toUpperCase();
if (normalized === 'EXTERNAL' || normalized === 'DATABASE') return normalized;
@@ -1952,9 +2040,16 @@ function createIngressManifest(input: {
path: string;
serviceName: string;
servicePort: number;
middlewareName?: string | null;
tlsEnabled: boolean;
tlsSecretName?: string | null;
}) {
const annotations: Record<string, string> = {};
if (input.middlewareName) {
annotations['traefik.ingress.kubernetes.io/router.middlewares'] = `${input.namespace}-${input.middlewareName}@kubernetescrd`;
}
const manifest: Record<string, unknown> = {
apiVersion: 'networking.k8s.io/v1',
kind: 'Ingress',
@@ -1962,6 +2057,7 @@ function createIngressManifest(input: {
name: input.name,
namespace: input.namespace,
labels: getManagedLabels(input.name, 'ingress'),
...(Object.keys(annotations).length > 0 ? { annotations } : {}),
},
spec: {
rules: [
@@ -1998,6 +2094,27 @@ function createIngressManifest(input: {
return manifest;
}
function createTraefikStripPrefixMiddlewareManifest(input: {
namespace: string;
name: string;
prefix: string;
}) {
return {
apiVersion: 'traefik.io/v1alpha1',
kind: 'Middleware',
metadata: {
name: input.name,
namespace: input.namespace,
labels: getManagedLabels(input.name, 'ingress-middleware'),
},
spec: {
stripPrefix: {
prefixes: [input.prefix],
},
},
};
}
function createPostgresSecretManifest(input: {
namespace: string;
name: string;