mirror of
https://github.com/labring/sealos.git
synced 2026-10-29 17:25:58 +08:00
feat(launchpad): update ports api & schema (#5914)
* update launchpad ports api * update schema * delete log * update
This commit is contained in:
@@ -80,7 +80,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
}
|
||||
|
||||
const updateData = parseResult.data;
|
||||
|
||||
await updateAppResources(name, updateData, k8s);
|
||||
|
||||
const response = await getAppByName(name, k8s);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/services/backend/response';
|
||||
import { createK8sContext, getAppByName, processAppResponse } from '@/services/backend';
|
||||
import { PatchUtils, V1Deployment, V1StatefulSet } from '@kubernetes/client-node';
|
||||
import { UpdatePortsSchema } from '@/types/request_schema';
|
||||
import { K8sContext, createK8sContext, getAppByName, processAppResponse } from '@/services/backend';
|
||||
import { V1Deployment, V1StatefulSet } from '@kubernetes/client-node';
|
||||
import { CreatePortsSchema, UpdatePortsSchema, DeletePortsSchema } from '@/types/request_schema';
|
||||
import { z } from 'zod';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { json2Service, json2Ingress } from '@/utils/deployYaml2Json';
|
||||
import type { AppEditType } from '@/types/app';
|
||||
@@ -11,7 +12,7 @@ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (req.method !== 'PATCH') {
|
||||
if (!['POST', 'PATCH', 'DELETE'].includes(req.method!)) {
|
||||
return jsonRes(res, {
|
||||
code: 405,
|
||||
error: `Method ${req.method} Not Allowed`
|
||||
@@ -24,17 +25,41 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const currentAppResponse = await getAppByName(appName, k8s);
|
||||
const currentAppData = await processAppResponse(currentAppResponse, false);
|
||||
|
||||
const parseResult = UpdatePortsSchema.safeParse(req.body);
|
||||
if (!parseResult.success) {
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
error: parseResult.error
|
||||
});
|
||||
if (req.method === 'DELETE') {
|
||||
const parseResult = DeletePortsSchema.safeParse(req.body);
|
||||
if (!parseResult.success) {
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
error: parseResult.error
|
||||
});
|
||||
}
|
||||
return await handleDeletePorts(parseResult.data.ports, appName, k8s, currentAppData, res);
|
||||
}
|
||||
|
||||
const { ports } = parseResult.data;
|
||||
const { k8sApp, namespace, applyYamlList } = k8s;
|
||||
let ports:
|
||||
| z.infer<typeof CreatePortsSchema>['ports']
|
||||
| z.infer<typeof UpdatePortsSchema>['ports'];
|
||||
if (req.method === 'POST') {
|
||||
const parseResult = CreatePortsSchema.safeParse(req.body);
|
||||
if (!parseResult.success) {
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
error: parseResult.error
|
||||
});
|
||||
}
|
||||
ports = parseResult.data.ports;
|
||||
} else {
|
||||
const parseResult = UpdatePortsSchema.safeParse(req.body);
|
||||
if (!parseResult.success) {
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
error: parseResult.error
|
||||
});
|
||||
}
|
||||
ports = parseResult.data.ports;
|
||||
}
|
||||
|
||||
const { k8sApp, namespace, applyYamlList } = k8s;
|
||||
let app: V1Deployment | V1StatefulSet;
|
||||
|
||||
const deploymentResult = currentAppResponse[0];
|
||||
@@ -58,7 +83,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
|
||||
// validate ports
|
||||
const uniquePorts = new Set(ports.map((p) => p.port));
|
||||
if (uniquePorts.size !== ports.length) {
|
||||
return jsonRes(res, {
|
||||
@@ -67,40 +91,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
|
||||
const networks = ports.map((port) => {
|
||||
// check if there is a unique identifier (networkName, portName, serviceName)
|
||||
// Only consider it an update if we can find an existing port with matching identifiers
|
||||
const existingPort = currentAppData.networks?.find(
|
||||
(n) =>
|
||||
n.port === port.port &&
|
||||
((port.networkName && n.networkName === port.networkName) ||
|
||||
(port.portName && n.portName === port.portName) ||
|
||||
(port.serviceName && n.serviceName === port.serviceName))
|
||||
);
|
||||
const isUpdate = !!existingPort;
|
||||
if (req.method === 'POST') {
|
||||
const existingPorts = currentAppData.networks?.map((n) => n.port) || [];
|
||||
const conflictPorts = ports.filter((p) => existingPorts.includes(p.port));
|
||||
if (conflictPorts.length > 0) {
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
error: `Ports already exist: ${conflictPorts.map((p) => p.port).join(', ')}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get existing port names to avoid conflicts
|
||||
const existingPortNames = new Set(currentAppData.networks?.map((n) => n.portName) || []);
|
||||
|
||||
const networks = ports!.map((port) => {
|
||||
if (req.method === 'POST') {
|
||||
// Generate unique port name that doesn't conflict with existing ones
|
||||
let portName = nanoid();
|
||||
while (existingPortNames.has(portName)) {
|
||||
portName = nanoid();
|
||||
}
|
||||
existingPortNames.add(portName);
|
||||
|
||||
if (isUpdate) {
|
||||
return {
|
||||
...existingPort,
|
||||
port: port.port,
|
||||
protocol: port.protocol,
|
||||
appProtocol: port.appProtocol || 'HTTP',
|
||||
openPublicDomain: port.exposesPublicDomain,
|
||||
openNodePort: !port.appProtocol,
|
||||
// Ensure required fields are always present
|
||||
networkName: existingPort?.networkName || `network-${nanoid()}`,
|
||||
portName: existingPort?.portName || nanoid(),
|
||||
serviceName: existingPort?.serviceName || `service-${nanoid()}`,
|
||||
publicDomain: existingPort?.publicDomain || nanoid(),
|
||||
domain: existingPort?.domain || global.AppConfig?.cloud?.domain || 'cloud.sealos.io',
|
||||
customDomain: existingPort?.customDomain || ''
|
||||
};
|
||||
} else {
|
||||
// For new ports, create complete configuration
|
||||
return {
|
||||
serviceName: `service-${nanoid()}`,
|
||||
networkName: `network-${nanoid()}`,
|
||||
portName: nanoid(),
|
||||
portName,
|
||||
port: port.port,
|
||||
protocol: port.protocol,
|
||||
appProtocol: port.appProtocol || 'HTTP',
|
||||
@@ -111,6 +128,40 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
nodePort: undefined,
|
||||
openNodePort: !port.appProtocol
|
||||
};
|
||||
} else {
|
||||
const portWithIds = port as z.infer<typeof UpdatePortsSchema>['ports'][0];
|
||||
const existingPort = currentAppData.networks?.find((n: any) => {
|
||||
// Match by specific identifiers in priority order
|
||||
if (portWithIds.portName && n.portName === portWithIds.portName) {
|
||||
return true;
|
||||
}
|
||||
if (portWithIds.networkName && n.networkName === portWithIds.networkName) {
|
||||
return true;
|
||||
}
|
||||
// Only match by serviceName if no other identifiers are provided
|
||||
if (
|
||||
!portWithIds.portName &&
|
||||
!portWithIds.networkName &&
|
||||
portWithIds.serviceName &&
|
||||
n.serviceName === portWithIds.serviceName
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (existingPort) {
|
||||
return {
|
||||
...existingPort,
|
||||
port: portWithIds.port,
|
||||
protocol: portWithIds.protocol,
|
||||
appProtocol: portWithIds.appProtocol || 'HTTP',
|
||||
openPublicDomain: portWithIds.exposesPublicDomain,
|
||||
openNodePort: !portWithIds.appProtocol
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Port not found for update: ${JSON.stringify(portWithIds)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -121,78 +172,40 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
protocol: network.protocol
|
||||
}));
|
||||
|
||||
const deploymentPatch = {
|
||||
spec: {
|
||||
template: {
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: app.spec.template.spec.containers[0].name,
|
||||
ports: containerPorts
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
await updateAppPorts(app, appName, k8sApp, namespace, containerPorts, req.method);
|
||||
|
||||
if (app.kind === 'Deployment') {
|
||||
await k8sApp.patchNamespacedDeployment(
|
||||
appName,
|
||||
namespace,
|
||||
deploymentPatch,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ headers: { 'Content-type': PatchUtils.PATCH_FORMAT_STRATEGIC_MERGE_PATCH } }
|
||||
);
|
||||
} else if (app.kind === 'StatefulSet') {
|
||||
await k8sApp.patchNamespacedStatefulSet(
|
||||
appName,
|
||||
namespace,
|
||||
deploymentPatch,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ headers: { 'Content-type': PatchUtils.PATCH_FORMAT_STRATEGIC_MERGE_PATCH } }
|
||||
);
|
||||
let mergedNetworks: any[];
|
||||
if (req.method === 'POST') {
|
||||
mergedNetworks = [...(currentAppData?.networks || []), ...networks];
|
||||
} else {
|
||||
throw new Error(`Unsupported app kind: ${app.kind}`);
|
||||
const updatedPortNames = new Set(networks.map((n) => n.portName));
|
||||
mergedNetworks = [
|
||||
...(currentAppData?.networks?.filter(
|
||||
(existing) => !updatedPortNames.has(existing.portName)
|
||||
) || []),
|
||||
...networks
|
||||
];
|
||||
}
|
||||
|
||||
const updatedPorts = new Set(networks.map((n) => n.port));
|
||||
|
||||
const mergedNetworks = [
|
||||
...(currentAppData?.networks?.filter((existing) => !updatedPorts.has(existing.port)) || []),
|
||||
|
||||
...networks
|
||||
];
|
||||
// Check for duplicate port names in mergedNetworks
|
||||
const portNames = mergedNetworks.map((n) => n.portName);
|
||||
const duplicatePortNames = portNames.filter(
|
||||
(name, index) => portNames.indexOf(name) !== index
|
||||
);
|
||||
if (duplicatePortNames.length > 0) {
|
||||
console.error('Duplicate port names detected:', duplicatePortNames);
|
||||
console.error(
|
||||
'All networks:',
|
||||
mergedNetworks.map((n) => ({ port: n.port, portName: n.portName }))
|
||||
);
|
||||
}
|
||||
|
||||
const appEditData: AppEditType = {
|
||||
...currentAppData,
|
||||
networks: mergedNetworks
|
||||
};
|
||||
console.log(appEditData, 'appEditData');
|
||||
|
||||
const yamlList: string[] = [];
|
||||
|
||||
const serviceYaml = json2Service(appEditData);
|
||||
if (serviceYaml.trim()) {
|
||||
yamlList.push(serviceYaml);
|
||||
}
|
||||
|
||||
const ingressYaml = json2Ingress(appEditData);
|
||||
if (ingressYaml.trim()) {
|
||||
yamlList.push(ingressYaml);
|
||||
}
|
||||
|
||||
if (yamlList.length > 0) {
|
||||
await applyYamlList(yamlList, 'replace');
|
||||
}
|
||||
await updateServiceAndIngress(appEditData, applyYamlList);
|
||||
|
||||
const response = await getAppByName(appName, k8s);
|
||||
const filteredData = await processAppResponse(response);
|
||||
@@ -201,10 +214,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
data: filteredData
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to update ports:', error);
|
||||
return jsonRes(res, {
|
||||
code: 500,
|
||||
error: error.message || 'Failed to update ports'
|
||||
error: error?.message || `Failed to ports`
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -215,3 +227,182 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAppPorts(
|
||||
app: V1Deployment | V1StatefulSet,
|
||||
appName: string,
|
||||
k8sApp: any,
|
||||
namespace: string,
|
||||
containerPorts: any[],
|
||||
method?: string
|
||||
) {
|
||||
let jsonPatch: any[];
|
||||
|
||||
if (method === 'POST') {
|
||||
const currentPorts = app.spec?.template?.spec?.containers[0]?.ports || [];
|
||||
const allPorts = [...currentPorts, ...containerPorts];
|
||||
jsonPatch = [
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/spec/template/spec/containers/0/ports',
|
||||
value: allPorts
|
||||
}
|
||||
];
|
||||
} else if (method === 'PATCH') {
|
||||
const currentPorts = app.spec?.template?.spec?.containers[0]?.ports || [];
|
||||
const updatedPorts = currentPorts.map((port: any) => {
|
||||
const updatedPort = containerPorts.find((cp: any) => cp.name === port.name);
|
||||
return updatedPort || port;
|
||||
});
|
||||
jsonPatch = [
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/spec/template/spec/containers/0/ports',
|
||||
value: updatedPorts
|
||||
}
|
||||
];
|
||||
} else if (method === 'DELETE') {
|
||||
const currentPorts = app.spec?.template?.spec?.containers[0]?.ports || [];
|
||||
const portsToDelete = new Set(containerPorts.map((cp: any) => cp.containerPort));
|
||||
const remainingPorts = currentPorts.filter(
|
||||
(port: any) => !portsToDelete.has(port.containerPort)
|
||||
);
|
||||
jsonPatch = [
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/spec/template/spec/containers/0/ports',
|
||||
value: remainingPorts
|
||||
}
|
||||
];
|
||||
} else {
|
||||
jsonPatch = [
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/spec/template/spec/containers/0/ports',
|
||||
value: containerPorts
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
if (app.kind === 'Deployment') {
|
||||
await k8sApp.patchNamespacedDeployment(
|
||||
appName,
|
||||
namespace,
|
||||
jsonPatch,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ headers: { 'Content-type': 'application/json-patch+json' } }
|
||||
);
|
||||
} else if (app.kind === 'StatefulSet') {
|
||||
await k8sApp.patchNamespacedStatefulSet(
|
||||
appName,
|
||||
namespace,
|
||||
jsonPatch,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ headers: { 'Content-type': 'application/json-patch+json' } }
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Unsupported app kind: ${app.kind}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateServiceAndIngress(appEditData: AppEditType, applyYamlList: any) {
|
||||
const yamlList: string[] = [];
|
||||
|
||||
const serviceYaml = json2Service(appEditData);
|
||||
if (serviceYaml.trim()) {
|
||||
yamlList.push(serviceYaml);
|
||||
}
|
||||
|
||||
const ingressYaml = json2Ingress(appEditData);
|
||||
if (ingressYaml.trim()) {
|
||||
yamlList.push(ingressYaml);
|
||||
}
|
||||
|
||||
if (yamlList.length > 0) {
|
||||
await applyYamlList(yamlList, 'replace');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeletePorts(
|
||||
portsToDelete: number[],
|
||||
appName: string,
|
||||
k8s: K8sContext,
|
||||
currentAppData: AppEditType,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const remainingNetworks =
|
||||
currentAppData.networks?.filter((network) => !portsToDelete.includes(network.port)) || [];
|
||||
|
||||
if (remainingNetworks.length === currentAppData.networks?.length) {
|
||||
return jsonRes(res, {
|
||||
code: 404,
|
||||
error: `No matching ports found to delete: ${portsToDelete.join(', ')}`
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const currentAppResponse = await getAppByName(appName, k8s);
|
||||
const { applyYamlList } = k8s;
|
||||
let app: V1Deployment | V1StatefulSet;
|
||||
|
||||
const deploymentResult = currentAppResponse[0];
|
||||
const statefulSetResult = currentAppResponse[1];
|
||||
|
||||
if (deploymentResult.status === 'fulfilled' && deploymentResult.value.body) {
|
||||
app = deploymentResult.value.body;
|
||||
} else if (statefulSetResult.status === 'fulfilled' && statefulSetResult.value.body) {
|
||||
app = statefulSetResult.value.body;
|
||||
} else {
|
||||
return jsonRes(res, {
|
||||
code: 404,
|
||||
error: `App ${appName} not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (!app?.spec?.template?.spec) {
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
error: `App ${appName} has invalid structure`
|
||||
});
|
||||
}
|
||||
|
||||
const containerPorts = remainingNetworks.map((network: any) => ({
|
||||
containerPort: network.port,
|
||||
name: network.portName,
|
||||
protocol: network.protocol
|
||||
}));
|
||||
|
||||
await updateAppPorts(app, appName, k8s.k8sApp, k8s.namespace, containerPorts, 'DELETE');
|
||||
|
||||
const appEditData: AppEditType = {
|
||||
...currentAppData,
|
||||
networks: remainingNetworks
|
||||
};
|
||||
|
||||
await updateServiceAndIngress(appEditData, applyYamlList);
|
||||
|
||||
const response = await getAppByName(appName, k8s);
|
||||
const filteredData = await processAppResponse(response);
|
||||
|
||||
return jsonRes(res, {
|
||||
data: filteredData
|
||||
});
|
||||
} catch (error: any) {
|
||||
return jsonRes(res, {
|
||||
code: 500,
|
||||
error: error.message || 'Failed to delete ports'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { str2Num } from '@/utils/tools';
|
||||
import { adaptAppDetail } from '@/utils/adapt';
|
||||
import { DeployKindsType, AppDetailType } from '@/types/app';
|
||||
import { z } from 'zod';
|
||||
import { LaunchpadApplicationSchema } from '@/types/schema';
|
||||
import { LaunchpadApplicationSchema, resourceConverters } from '@/types/schema';
|
||||
import { transformFromLegacySchema } from '@/types/request_schema';
|
||||
import {
|
||||
PatchUtils,
|
||||
@@ -550,31 +550,33 @@ export async function updateAppResources(
|
||||
}> = [];
|
||||
|
||||
if (updateData.resource?.cpu !== undefined) {
|
||||
const millicores = resourceConverters.cpuToMillicores(updateData.resource.cpu);
|
||||
jsonPatch.push(
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/spec/template/spec/containers/0/resources/requests/cpu',
|
||||
value: `${str2Num(Math.floor(updateData.resource.cpu * 0.1))}m`
|
||||
value: `${str2Num(Math.floor(millicores * 0.1))}m`
|
||||
},
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/spec/template/spec/containers/0/resources/limits/cpu',
|
||||
value: `${str2Num(updateData.resource.cpu)}m`
|
||||
value: `${str2Num(millicores)}m`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (updateData.resource?.memory !== undefined) {
|
||||
const memoryMB = resourceConverters.memoryToMB(updateData.resource.memory);
|
||||
jsonPatch.push(
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/spec/template/spec/containers/0/resources/requests/memory',
|
||||
value: `${str2Num(Math.floor(updateData.resource.memory * 0.1))}Mi`
|
||||
value: `${str2Num(Math.floor(memoryMB * 0.1))}Mi`
|
||||
},
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/spec/template/spec/containers/0/resources/limits/memory',
|
||||
value: `${str2Num(updateData.resource.memory)}Mi`
|
||||
value: `${str2Num(memoryMB)}Mi`
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -625,6 +627,12 @@ export async function updateAppResources(
|
||||
path: '/spec/template/spec/containers/0/image',
|
||||
value: updateData.image
|
||||
});
|
||||
|
||||
jsonPatch.push({
|
||||
op: 'replace',
|
||||
path: '/metadata/annotations/originImageName',
|
||||
value: updateData.image
|
||||
});
|
||||
}
|
||||
|
||||
if (updateData.env !== undefined) {
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
GetAppByAppNameResponseSchema,
|
||||
UpdateAppResourcesSchema,
|
||||
UpdateConfigMapSchema,
|
||||
CreatePortsSchema,
|
||||
UpdatePortsSchema,
|
||||
DeletePortsSchema,
|
||||
UpdateStorageSchema
|
||||
} from './request_schema';
|
||||
|
||||
@@ -426,12 +428,74 @@ export const openApiDocument = (sealosDomain: string) =>
|
||||
}
|
||||
},
|
||||
'/api/v1/app/{name}/ports': {
|
||||
patch: {
|
||||
summary: 'Update application ports',
|
||||
post: {
|
||||
summary: 'Create new application ports',
|
||||
description:
|
||||
'Update application port configurations including container ports, services, and ingresses. ' +
|
||||
'This API applies YAML-based updates to network resources, preserving existing configurations ' +
|
||||
'for ports not specified in the request. Only the specified ports will be updated or added.',
|
||||
'Add new port configurations to an application including container ports, services, and ingresses. ' +
|
||||
'This API creates entirely new ports that do not already exist in the application. ' +
|
||||
'Port conflicts will be rejected.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'name',
|
||||
in: 'path',
|
||||
description: 'Application name',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CreatePortsSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Ports created successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
data: LaunchpadApplicationSchema
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
'400': {
|
||||
description: 'Invalid request parameters, duplicate ports, or ports already exist',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
'404': {
|
||||
description: 'Application not found',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
'500': {
|
||||
description: 'Internal server error during ports creation',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
patch: {
|
||||
summary: 'Update existing application ports',
|
||||
description:
|
||||
'Update existing application port configurations including container ports, services, and ingresses. ' +
|
||||
'This API requires at least one identifier (networkName, portName, or serviceName) to locate the port to update. ' +
|
||||
'Only specified ports will be updated, existing ports not mentioned will remain unchanged.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'name',
|
||||
@@ -463,7 +527,7 @@ export const openApiDocument = (sealosDomain: string) =>
|
||||
}
|
||||
},
|
||||
'400': {
|
||||
description: 'Invalid request parameters or application structure',
|
||||
description: 'Invalid request parameters, missing identifiers, or port not found',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
@@ -487,6 +551,68 @@ export const openApiDocument = (sealosDomain: string) =>
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
delete: {
|
||||
summary: 'Delete application ports',
|
||||
description:
|
||||
'Delete specified application ports by port number. ' +
|
||||
'This API removes the specified ports from container configuration, services, and ingresses. ' +
|
||||
'At least one port number must be provided.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'name',
|
||||
in: 'path',
|
||||
description: 'Application name',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: DeletePortsSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Ports deleted successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
data: LaunchpadApplicationSchema
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
'400': {
|
||||
description: 'Invalid request parameters or application structure',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
'404': {
|
||||
description: 'Application not found or no matching ports found to delete',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
'500': {
|
||||
description: 'Internal server error during ports deletion',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/v1/app/{name}/storage': {
|
||||
|
||||
@@ -31,7 +31,8 @@ import {
|
||||
ResourceSchema,
|
||||
PortConfigSchema,
|
||||
LaunchpadApplicationSchema,
|
||||
imageRegistrySchema
|
||||
imageRegistrySchema,
|
||||
resourceConverters
|
||||
} from './schema';
|
||||
|
||||
export const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12);
|
||||
@@ -55,15 +56,40 @@ export const UpdateAppResourcesSchema = z
|
||||
// Resource configuration (use nested structure like CreateLaunchpadRequestSchema)
|
||||
resource: z
|
||||
.object({
|
||||
cpu: z.number().optional().openapi({
|
||||
description: 'CPU allocation in millicores'
|
||||
}),
|
||||
memory: z.number().optional().openapi({
|
||||
description: 'Memory allocation in MB'
|
||||
}),
|
||||
replicas: z.number().min(0).optional().openapi({
|
||||
description: 'Number of pod replicas'
|
||||
})
|
||||
cpu: z
|
||||
.number()
|
||||
.refine((val) => [0.1, 0.2, 0.5, 1, 2, 3, 4, 8].includes(val), {
|
||||
message: 'CPU must be one of: 0.1, 0.2, 0.5, 1, 2, 3, 4, 8'
|
||||
})
|
||||
.optional()
|
||||
.openapi({
|
||||
description: 'CPU allocation in cores',
|
||||
enum: [0.1, 0.2, 0.5, 1, 2, 3, 4, 8]
|
||||
}),
|
||||
memory: z
|
||||
.number()
|
||||
.refine((val) => [0.1, 0.5, 1, 2, 4, 8, 16].includes(val), {
|
||||
message: 'Memory must be one of: 0.1, 0.5, 1, 2, 4, 8, 16'
|
||||
})
|
||||
.optional()
|
||||
.openapi({
|
||||
description: 'Memory allocation in GB',
|
||||
enum: [0.1, 0.5, 1, 2, 4, 8, 16]
|
||||
}),
|
||||
replicas: z
|
||||
.number()
|
||||
.refine(
|
||||
(val) =>
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].includes(val),
|
||||
{
|
||||
message: 'Replicas must be between 1 and 20'
|
||||
}
|
||||
)
|
||||
.optional()
|
||||
.openapi({
|
||||
description: 'Number of pod replicas',
|
||||
enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||
})
|
||||
})
|
||||
.optional()
|
||||
.openapi({
|
||||
@@ -217,8 +243,8 @@ export const CreateLaunchpadRequestSchema = z
|
||||
export function transformToLegacySchema(
|
||||
standardRequest: z.infer<typeof CreateLaunchpadRequestSchema>
|
||||
): AppEditType {
|
||||
const cpuValue = standardRequest.resource.cpu;
|
||||
const memoryValue = standardRequest.resource.memory;
|
||||
const cpuValue = resourceConverters.cpuToMillicores(standardRequest.resource.cpu);
|
||||
const memoryValue = resourceConverters.memoryToMB(standardRequest.resource.memory);
|
||||
|
||||
const networks = standardRequest.ports?.map((port) => ({
|
||||
serviceName: `service-${nanoid()}`, // 自动生成
|
||||
@@ -357,8 +383,8 @@ export function transformFromLegacySchema(
|
||||
args: legacyData.cmdParam,
|
||||
resource: {
|
||||
replicas: legacyData.replicas || 1,
|
||||
cpu: legacyData.cpu || 200,
|
||||
memory: legacyData.memory || 256,
|
||||
cpu: resourceConverters.millicoresToCpu(legacyData.cpu || 200),
|
||||
memory: resourceConverters.mbToMemory(legacyData.memory || 256),
|
||||
gpu: legacyData.gpu
|
||||
? {
|
||||
vendor: legacyData.gpu.manufacturers,
|
||||
@@ -439,27 +465,57 @@ export const UpdateConfigMapSchema = z.object({
|
||||
})
|
||||
});
|
||||
|
||||
export const UpdatePortsSchema = z.object({
|
||||
// POST - Create new ports (no identifiers needed)
|
||||
export const CreatePortsSchema = z.object({
|
||||
ports: z
|
||||
.array(
|
||||
z.object({
|
||||
port: z.number().default(80),
|
||||
protocol: z.enum(['TCP', 'UDP', 'SCTP']),
|
||||
appProtocol: z.enum(['HTTP', 'GRPC', 'WS']).optional(),
|
||||
exposesPublicDomain: z.boolean(),
|
||||
|
||||
networkName: z.string().optional(),
|
||||
portName: z.string().optional(),
|
||||
serviceName: z.string().optional()
|
||||
exposesPublicDomain: z.boolean()
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.openapi({
|
||||
description:
|
||||
'Port/Network configurations to update. Include networkName/portName/serviceName for updates, omit for new ports'
|
||||
description: 'Port configurations to create (new ports only)'
|
||||
})
|
||||
});
|
||||
|
||||
// PATCH - Update existing ports (requires identifiers to locate)
|
||||
export const UpdatePortsSchema = z.object({
|
||||
ports: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
port: z.number(),
|
||||
protocol: z.enum(['TCP', 'UDP', 'SCTP']),
|
||||
appProtocol: z.enum(['HTTP', 'GRPC', 'WS']).optional(),
|
||||
exposesPublicDomain: z.boolean(),
|
||||
// At least one identifier required to locate the port to update
|
||||
networkName: z.string().optional(),
|
||||
portName: z.string().optional(),
|
||||
serviceName: z.string().optional()
|
||||
})
|
||||
.refine((data) => data.networkName || data.portName || data.serviceName, {
|
||||
message:
|
||||
'At least one identifier (networkName/portName/serviceName) is required to locate the port to update'
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.openapi({
|
||||
description:
|
||||
'Port configurations to update. Must include at least one identifier (networkName/portName/serviceName) to locate existing port'
|
||||
})
|
||||
});
|
||||
|
||||
// DELETE - Delete ports by port number
|
||||
export const DeletePortsSchema = z.object({
|
||||
ports: z.array(z.number()).min(1).openapi({
|
||||
description: 'Array of port numbers to delete'
|
||||
})
|
||||
});
|
||||
|
||||
export const SimpleStorageSchema = z
|
||||
.object({
|
||||
path: z.string().openapi({
|
||||
|
||||
@@ -22,16 +22,56 @@ import { customAlphabet } from 'nanoid';
|
||||
|
||||
export const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12);
|
||||
|
||||
// Conversion utilities for resource values
|
||||
export const resourceConverters = {
|
||||
// Convert CPU cores to millicores (e.g., 0.1 -> 100, 1 -> 1000)
|
||||
cpuToMillicores: (cores: number): number => cores * 1000,
|
||||
|
||||
// Convert GB to MB (e.g., 0.5 -> 512, 1 -> 1024)
|
||||
memoryToMB: (gb: number): number => gb * 1024,
|
||||
|
||||
// Convert millicores to cores for display (e.g., 100 -> 0.1, 1000 -> 1)
|
||||
millicoresToCpu: (millicores: number): number => millicores / 1000,
|
||||
|
||||
// Convert MB to GB for display (e.g., 512 -> 0.5, 1024 -> 1)
|
||||
mbToMemory: (mb: number): number => mb / 1024
|
||||
};
|
||||
|
||||
export const ResourceSchema = z.object({
|
||||
replicas: z.number().min(0).max(10).default(1).openapi({
|
||||
description: 'Number of pod replicas'
|
||||
}),
|
||||
cpu: z.number().default(200).openapi({
|
||||
description: 'CPU allocation in millicores'
|
||||
}),
|
||||
memory: z.number().default(256).openapi({
|
||||
description: 'Memory allocation in MB'
|
||||
}),
|
||||
replicas: z
|
||||
.number()
|
||||
.refine(
|
||||
(val) =>
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].includes(val),
|
||||
{
|
||||
message: 'Replicas must be between 1 and 20'
|
||||
}
|
||||
)
|
||||
.default(1)
|
||||
.openapi({
|
||||
description: 'Number of pod replicas',
|
||||
enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||
}),
|
||||
cpu: z
|
||||
.number()
|
||||
.refine((val) => [0.1, 0.2, 0.5, 1, 2, 3, 4, 8].includes(val), {
|
||||
message: 'CPU must be one of: 0.1, 0.2, 0.5, 1, 2, 3, 4, 8'
|
||||
})
|
||||
.default(0.2)
|
||||
.openapi({
|
||||
description: 'CPU allocation in cores',
|
||||
enum: [0.1, 0.2, 0.5, 1, 2, 3, 4, 8]
|
||||
}),
|
||||
memory: z
|
||||
.number()
|
||||
.refine((val) => [0.1, 0.5, 1, 2, 4, 8, 16].includes(val), {
|
||||
message: 'Memory must be one of: 0.1, 0.5, 1, 2, 4, 8, 16'
|
||||
})
|
||||
.default(0.5)
|
||||
.openapi({
|
||||
description: 'Memory allocation in GB',
|
||||
enum: [0.1, 0.5, 1, 2, 4, 8, 16]
|
||||
}),
|
||||
gpu: z
|
||||
.object({
|
||||
vendor: z.string().default('nvidia'),
|
||||
|
||||
Reference in New Issue
Block a user