Files
cloudly/ts/manager.service/classes.servicemanager.ts
T

326 lines
12 KiB
TypeScript

import type { Cloudly } from '../classes.cloudly.js';
import * as plugins from '../plugins.js';
import { Service } from './classes.service.js';
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;
}
export class ServiceManager {
public typedrouter = new plugins.typedrequest.TypedRouter();
public cloudlyRef: Cloudly;
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CService = plugins.smartdata.setDefaultManagerForDoc(this, Service);
constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
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();
})
),
};
}
)
);
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(),
};
}
)
);
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,
};
}
)
);
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',
),
};
}
)
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_CreateService>(
'createService',
async (dataArg) => {
await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.adminIdentityGuard,
]);
const service = await Service.createService(dataArg.serviceData);
service.data.registryTarget = this.cloudlyRef.registryManager.getServiceRegistryTarget(
service,
service.data.imageVersion || 'latest',
);
await service.save();
await this.cloudlyRef.coreflowManager.pushClusterConfigToConnectedCoreflows();
return {
service: await service.createSavableObject(),
};
}
)
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(
'updateService',
async (dataArg) => {
await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.adminIdentityGuard,
]);
const service = await Service.getInstance({
id: dataArg.serviceId,
});
service.data = {
...service.data,
...dataArg.serviceData,
};
service.data.registryTarget = this.cloudlyRef.registryManager.getServiceRegistryTarget(
service,
service.data.imageVersion || 'latest',
);
await service.save();
await this.cloudlyRef.coreflowManager.pushClusterConfigToConnectedCoreflows();
return {
service: await service.createSavableObject(),
};
}
)
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(
'deleteServiceById',
async (dataArg) => {
await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.adminIdentityGuard,
]);
await this.deleteServiceById(dataArg.serviceId);
return {
success: true,
};
}
)
);
}
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');
}
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;
}
}