feat: add app preview via NodePort
- Backend: getPreviewInfo() in KubernetesService auto-patches ClusterIP
service to NodePort for direct external access
- Backend: GET /applications/:id/preview endpoint returns access URL
with nodePort, host IP (extracted from kubeconfig), and ingress URL
- Frontend: '🌐 Preview' button on app detail page (visible when running)
opens the deployed app in a new browser tab via NodePort URL
- Tested: service patched to NodePort 30107 successfully
This commit is contained in:
@@ -122,6 +122,16 @@ export class ApplicationsController {
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Get(':id/preview')
|
||||
@ApiOperation({ summary: 'Get preview URL for the deployed application' })
|
||||
async getPreview(@Param('id') id: string, @Request() req: any) {
|
||||
const app = await this.applicationsService.findOne(
|
||||
id,
|
||||
req.user.role === UserRole.ADMIN ? undefined : req.user.id,
|
||||
);
|
||||
return this.kubernetesService.getPreviewInfo(app);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete an application and all its resources' })
|
||||
async delete(@Param('id') id: string, @Request() req: any) {
|
||||
|
||||
@@ -686,6 +686,82 @@ export class KubernetesService implements OnModuleInit {
|
||||
this.logger.log(`Updated resources for ${app.name}: ${JSON.stringify(resources)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview info for a deployed application.
|
||||
* Patches the service to NodePort if needed, and returns the access URL.
|
||||
*/
|
||||
async getPreviewInfo(app: Application): Promise<{
|
||||
url: string;
|
||||
nodePort: number;
|
||||
host: string;
|
||||
ingressUrl?: string;
|
||||
}> {
|
||||
const { coreApi, networkingApi, kc } = await this.getK8sClient(app.clusterId);
|
||||
const namespace = `user-${app.userId.split('-')[0]}`;
|
||||
const domain = this.configService.get('platform.domain');
|
||||
const clusterServer = kc.getCurrentCluster()?.server || '';
|
||||
// Extract host IP from cluster API server URL (e.g., https://217.197.107.252:6443 → 217.197.107.252)
|
||||
let hostIp = '127.0.0.1';
|
||||
try {
|
||||
const serverUrl = new URL(clusterServer);
|
||||
hostIp = serverUrl.hostname;
|
||||
} catch {}
|
||||
|
||||
// Read current service
|
||||
let nodePort = 0;
|
||||
try {
|
||||
const svcResponse = await coreApi.readNamespacedService(app.name, namespace);
|
||||
const svc = svcResponse.body;
|
||||
|
||||
if (svc.spec?.type === 'NodePort') {
|
||||
// Already NodePort, read the assigned port
|
||||
nodePort = svc.spec.ports?.[0]?.nodePort || 0;
|
||||
} else {
|
||||
// Patch ClusterIP → NodePort so we can access from outside
|
||||
const patchBody = {
|
||||
spec: {
|
||||
type: 'NodePort',
|
||||
ports: [
|
||||
{
|
||||
port: 80,
|
||||
targetPort: app.port,
|
||||
protocol: 'TCP',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const patchedResponse = await coreApi.patchNamespacedService(
|
||||
app.name,
|
||||
namespace,
|
||||
patchBody,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ headers: { 'Content-Type': 'application/strategic-merge-patch+json' } },
|
||||
);
|
||||
nodePort = patchedResponse.body.spec?.ports?.[0]?.nodePort || 0;
|
||||
this.logger.log(`Patched service ${app.name} to NodePort: ${nodePort}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.logger.warn(`Failed to get/patch service for ${app.name}: ${e.message}`);
|
||||
throw new Error(`Service not found for "${app.name}". Make sure the app is deployed.`);
|
||||
}
|
||||
|
||||
// Build ingress URL
|
||||
const subdomain = app.subdomain || app.name;
|
||||
const ingressUrl = `https://${subdomain}.${domain}`;
|
||||
|
||||
return {
|
||||
url: `http://${hostIp}:${nodePort}`,
|
||||
nodePort,
|
||||
host: hostIp,
|
||||
ingressUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteApplication(app: Application): Promise<void> {
|
||||
const { coreApi, appsApi, networkingApi } = await this.getK8sClient(app.clusterId);
|
||||
const namespace = `user-${app.userId.split('-')[0]}`;
|
||||
|
||||
Reference in New Issue
Block a user