2024-06-13 09:36:02 +02:00
|
|
|
import type { Cloudly } from '../classes.cloudly.js';
|
|
|
|
|
import * as plugins from '../plugins.js';
|
2024-06-13 10:07:53 +02:00
|
|
|
import { Service } from './classes.service.js';
|
2024-06-13 09:36:02 +02:00
|
|
|
|
2026-05-28 16:13:06 +00:00
|
|
|
type TServiceWithDomains = Service & {
|
|
|
|
|
data: Service['data'] & {
|
|
|
|
|
appTemplateId?: string;
|
|
|
|
|
domains?: Array<{ name?: string }>;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface IWorkAppRouteSyncResult {
|
|
|
|
|
success: boolean;
|
|
|
|
|
action?: 'created' | 'updated' | 'deleted' | 'unchanged';
|
|
|
|
|
routeId?: string;
|
|
|
|
|
message?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-13 09:36:02 +02:00
|
|
|
export class ServiceManager {
|
|
|
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
|
|
|
public cloudlyRef: Cloudly;
|
2024-10-27 19:50:39 +01:00
|
|
|
|
2024-06-13 09:36:02 +02:00
|
|
|
get db() {
|
|
|
|
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public CService = plugins.smartdata.setDefaultManagerForDoc(this, Service);
|
2024-06-13 10:07:53 +02:00
|
|
|
|
|
|
|
|
constructor(cloudlyRef: Cloudly) {
|
|
|
|
|
this.cloudlyRef = cloudlyRef;
|
2025-09-08 12:46:23 +00:00
|
|
|
|
|
|
|
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
2024-12-14 20:32:17 +01:00
|
|
|
|
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServices>(
|
|
|
|
|
'getServices',
|
|
|
|
|
async (reqArg) => {
|
|
|
|
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
|
|
|
|
this.cloudlyRef.authManager.validIdentityGuard,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const services = await this.CService.getInstances({});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
services: await Promise.all(
|
|
|
|
|
services.map((service) => {
|
|
|
|
|
return service.createSavableObject();
|
|
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
2025-01-20 02:11:12 +01:00
|
|
|
|
2026-04-28 15:50:59 +00:00
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServiceById>(
|
|
|
|
|
'getServiceById',
|
|
|
|
|
async (dataArg) => {
|
|
|
|
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
|
|
|
|
this.cloudlyRef.authManager.validIdentityGuard,
|
|
|
|
|
]);
|
|
|
|
|
const service = await Service.getInstance({
|
|
|
|
|
id: dataArg.serviceId,
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
service: await service.createSavableObject(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
2025-01-20 02:11:12 +01:00
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServiceSecretBundlesAsFlatObject>(
|
|
|
|
|
'getServiceSecretBundlesAsFlatObject',
|
|
|
|
|
async (dataArg) => {
|
|
|
|
|
const service = await Service.getInstance({
|
|
|
|
|
id: dataArg.serviceId,
|
|
|
|
|
});
|
|
|
|
|
const flatKeyValueObject = await service.getSecretBundlesAsFlatObject(dataArg.environment);
|
|
|
|
|
return {
|
|
|
|
|
flatKeyValueObject: flatKeyValueObject,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-28 15:50:59 +00:00
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServiceRegistryTarget>(
|
|
|
|
|
'getServiceRegistryTarget',
|
|
|
|
|
async (dataArg) => {
|
|
|
|
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
|
|
|
|
this.cloudlyRef.authManager.validIdentityGuard,
|
|
|
|
|
]);
|
|
|
|
|
const service = await Service.getInstance({
|
|
|
|
|
id: dataArg.serviceId,
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
registryTarget: this.cloudlyRef.registryManager.getServiceRegistryTarget(
|
|
|
|
|
service,
|
|
|
|
|
dataArg.tag || service.data.imageVersion || 'latest',
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
2025-01-20 02:11:12 +01:00
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_CreateService>(
|
|
|
|
|
'createService',
|
|
|
|
|
async (dataArg) => {
|
2026-05-08 13:56:20 +00:00
|
|
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
|
|
|
|
this.cloudlyRef.authManager.adminIdentityGuard,
|
|
|
|
|
]);
|
2025-01-20 02:11:12 +01:00
|
|
|
const service = await Service.createService(dataArg.serviceData);
|
2026-04-28 15:50:59 +00:00
|
|
|
service.data.registryTarget = this.cloudlyRef.registryManager.getServiceRegistryTarget(
|
|
|
|
|
service,
|
|
|
|
|
service.data.imageVersion || 'latest',
|
|
|
|
|
);
|
|
|
|
|
await service.save();
|
2026-04-28 16:02:05 +00:00
|
|
|
await this.cloudlyRef.coreflowManager.pushClusterConfigToConnectedCoreflows();
|
2025-01-20 02:11:12 +01:00
|
|
|
return {
|
|
|
|
|
service: await service.createSavableObject(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
2025-01-20 02:18:58 +01:00
|
|
|
|
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(
|
|
|
|
|
'updateService',
|
|
|
|
|
async (dataArg) => {
|
2026-05-08 13:56:20 +00:00
|
|
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
|
|
|
|
this.cloudlyRef.authManager.adminIdentityGuard,
|
|
|
|
|
]);
|
2025-01-20 02:18:58 +01:00
|
|
|
const service = await Service.getInstance({
|
|
|
|
|
id: dataArg.serviceId,
|
|
|
|
|
});
|
|
|
|
|
service.data = {
|
|
|
|
|
...service.data,
|
|
|
|
|
...dataArg.serviceData,
|
|
|
|
|
};
|
2026-04-28 15:50:59 +00:00
|
|
|
service.data.registryTarget = this.cloudlyRef.registryManager.getServiceRegistryTarget(
|
|
|
|
|
service,
|
|
|
|
|
service.data.imageVersion || 'latest',
|
|
|
|
|
);
|
2025-01-20 02:18:58 +01:00
|
|
|
await service.save();
|
2026-04-28 16:02:05 +00:00
|
|
|
await this.cloudlyRef.coreflowManager.pushClusterConfigToConnectedCoreflows();
|
2025-01-20 02:18:58 +01:00
|
|
|
return {
|
|
|
|
|
service: await service.createSavableObject(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(
|
|
|
|
|
'deleteServiceById',
|
|
|
|
|
async (dataArg) => {
|
2026-05-08 13:56:20 +00:00
|
|
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
|
|
|
|
this.cloudlyRef.authManager.adminIdentityGuard,
|
|
|
|
|
]);
|
2026-05-28 16:13:06 +00:00
|
|
|
await this.deleteServiceById(dataArg.serviceId);
|
2025-01-20 02:18:58 +01:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
2024-06-13 10:07:53 +02:00
|
|
|
}
|
2025-09-08 12:46:23 +00:00
|
|
|
|
|
|
|
|
public async start() {
|
|
|
|
|
// ServiceManager is ready - handlers are already registered in constructor
|
|
|
|
|
console.log('ServiceManager started');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async stop() {
|
|
|
|
|
// Cleanup if needed
|
|
|
|
|
console.log('ServiceManager stopped');
|
|
|
|
|
}
|
2026-05-28 16:13:06 +00:00
|
|
|
|
|
|
|
|
public async deleteServiceById(serviceIdArg: string): Promise<void> {
|
|
|
|
|
const service = await this.CService.getInstance({
|
|
|
|
|
id: serviceIdArg,
|
|
|
|
|
});
|
|
|
|
|
if (!service) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError(`Service not found: ${serviceIdArg}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.deleteExternalGatewayRoutes(service as TServiceWithDomains);
|
|
|
|
|
this.cloudlyRef.appStoreManager?.clearOperationsForService?.(service.id);
|
|
|
|
|
await this.deleteDeploymentsForService(service.id);
|
|
|
|
|
await service.removeDnsEntries();
|
|
|
|
|
await this.deletePlatformBindingsForService(service.id, service.data.name);
|
|
|
|
|
await this.cloudlyRef.backupManager?.deleteBackupsForService?.(service.id);
|
|
|
|
|
await this.cloudlyRef.registryManager?.deleteServiceRepository?.(service);
|
|
|
|
|
await this.deleteServiceOwnedSecretBundles(service);
|
|
|
|
|
await this.deleteServiceOwnedImage(service as TServiceWithDomains);
|
|
|
|
|
await service.delete();
|
|
|
|
|
await this.cloudlyRef.coreflowManager.pushClusterConfigToConnectedCoreflows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async deleteDeploymentsForService(serviceIdArg: string): Promise<void> {
|
|
|
|
|
const deployments = await this.cloudlyRef.deploymentManager.CDeployment.getInstances({
|
|
|
|
|
serviceId: serviceIdArg,
|
|
|
|
|
});
|
|
|
|
|
for (const deployment of deployments) {
|
|
|
|
|
await deployment.delete();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async deletePlatformBindingsForService(
|
|
|
|
|
serviceIdArg: string,
|
|
|
|
|
serviceNameArg: string,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const bindingsById = await this.cloudlyRef.platformManager.CPlatformBinding.getInstances({
|
|
|
|
|
serviceId: serviceIdArg,
|
|
|
|
|
});
|
|
|
|
|
const bindingsByName = serviceNameArg
|
|
|
|
|
? await this.cloudlyRef.platformManager.CPlatformBinding.getInstances({ serviceId: serviceNameArg })
|
|
|
|
|
: [];
|
|
|
|
|
const bindings = new Map<string, typeof bindingsById[number]>();
|
|
|
|
|
for (const binding of [...bindingsById, ...bindingsByName]) {
|
|
|
|
|
bindings.set(binding.id, binding);
|
|
|
|
|
}
|
|
|
|
|
for (const binding of bindings.values()) {
|
|
|
|
|
await binding.delete();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async deleteServiceOwnedSecretBundles(serviceArg: Service): Promise<void> {
|
|
|
|
|
const secretBundleIds = [serviceArg.data.secretBundleId]
|
|
|
|
|
.filter((secretBundleIdArg): secretBundleIdArg is string => Boolean(secretBundleIdArg));
|
|
|
|
|
if (secretBundleIds.length === 0) return;
|
|
|
|
|
|
|
|
|
|
for (const secretBundleId of secretBundleIds) {
|
|
|
|
|
const secretBundle = await this.cloudlyRef.secretManager.CSecretBundle.getInstance({
|
|
|
|
|
id: secretBundleId,
|
|
|
|
|
}).catch(() => null);
|
|
|
|
|
if (!secretBundle || (secretBundle.data as { serviceId?: string }).serviceId !== serviceArg.id) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const secretGroupIds = [...(secretBundle.data.includedSecretGroupIds || [])];
|
|
|
|
|
await secretBundle.delete();
|
|
|
|
|
await this.deleteUnreferencedSecretGroups(secretGroupIds);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async deleteUnreferencedSecretGroups(secretGroupIdsArg: string[]): Promise<void> {
|
|
|
|
|
if (secretGroupIdsArg.length === 0) return;
|
|
|
|
|
const remainingBundles = await this.cloudlyRef.secretManager.CSecretBundle.getInstances({});
|
|
|
|
|
for (const secretGroupId of secretGroupIdsArg) {
|
|
|
|
|
const stillReferenced = remainingBundles.some((bundleArg) => {
|
|
|
|
|
return (bundleArg.data.includedSecretGroupIds || []).includes(secretGroupId);
|
|
|
|
|
});
|
|
|
|
|
if (stillReferenced) continue;
|
|
|
|
|
const secretGroup = await this.cloudlyRef.secretManager.CSecretGroup.getInstance({
|
|
|
|
|
id: secretGroupId,
|
|
|
|
|
}).catch(() => null);
|
|
|
|
|
if (secretGroup) {
|
|
|
|
|
await secretGroup.delete();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async deleteServiceOwnedImage(serviceArg: TServiceWithDomains): Promise<void> {
|
|
|
|
|
if (!serviceArg.data.appTemplateId || !serviceArg.data.imageId) return;
|
|
|
|
|
await this.cloudlyRef.imageManager.deleteImageIfUnreferenced(serviceArg.data.imageId, serviceArg.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async deleteExternalGatewayRoutes(serviceArg: TServiceWithDomains): Promise<void> {
|
|
|
|
|
const domains = (serviceArg.data.domains || [])
|
|
|
|
|
.map((domainArg) => domainArg.name?.trim().toLowerCase())
|
|
|
|
|
.filter((domainArg): domainArg is string => Boolean(domainArg));
|
|
|
|
|
if (domains.length === 0) return;
|
|
|
|
|
|
|
|
|
|
const settings = await this.cloudlyRef.settingsManager.getSettings().catch(() => undefined);
|
|
|
|
|
if (!settings?.dcrouterGatewayUrl || !settings.dcrouterGatewayApiToken) return;
|
|
|
|
|
|
|
|
|
|
const clusters = await this.cloudlyRef.clusterManager.getAllClusters().catch(() => []);
|
|
|
|
|
const workHosterIds = new Set<string>();
|
|
|
|
|
if (settings.dcrouterWorkHosterId) {
|
|
|
|
|
workHosterIds.add(settings.dcrouterWorkHosterId);
|
|
|
|
|
} else {
|
|
|
|
|
for (const cluster of clusters) {
|
|
|
|
|
workHosterIds.add(cluster.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (workHosterIds.size === 0) return;
|
|
|
|
|
|
|
|
|
|
for (const domain of domains) {
|
|
|
|
|
for (const workHosterId of workHosterIds) {
|
|
|
|
|
const result = await this.fireDcRouterRequest<IWorkAppRouteSyncResult>(
|
|
|
|
|
settings.dcrouterGatewayUrl,
|
|
|
|
|
'syncWorkAppRoute',
|
|
|
|
|
{
|
|
|
|
|
apiToken: settings.dcrouterGatewayApiToken,
|
|
|
|
|
ownership: {
|
|
|
|
|
workHosterType: 'cloudly',
|
|
|
|
|
workHosterId,
|
|
|
|
|
workAppId: serviceArg.id || serviceArg.data.name,
|
|
|
|
|
hostname: domain,
|
|
|
|
|
},
|
|
|
|
|
delete: true,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
throw new Error(result.message || `dcrouter route delete failed for ${domain}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async fireDcRouterRequest<TResponse>(
|
|
|
|
|
gatewayUrlArg: string,
|
|
|
|
|
methodArg: string,
|
|
|
|
|
requestDataArg: Record<string, unknown>,
|
|
|
|
|
): Promise<TResponse> {
|
|
|
|
|
const typedRequest = new plugins.typedrequest.TypedRequest<any>(
|
|
|
|
|
`${gatewayUrlArg.replace(/\/+$/, '')}/typedrequest`,
|
|
|
|
|
methodArg,
|
|
|
|
|
);
|
|
|
|
|
return await typedRequest.fire(requestDataArg) as TResponse;
|
|
|
|
|
}
|
2024-10-27 19:50:39 +01:00
|
|
|
}
|