294 lines
10 KiB
TypeScript
294 lines
10 KiB
TypeScript
import * as plugins from '../../plugins.ts';
|
|
import { logger } from '../../logging.ts';
|
|
import type { OpsServer } from '../classes.opsserver.ts';
|
|
import * as interfaces from '../../../ts_interfaces/index.ts';
|
|
import { requireAdminIdentity } from '../helpers/guards.ts';
|
|
import { getErrorMessage } from '../../utils/error.ts';
|
|
|
|
type IAppStoreUpgradeOperation = interfaces.requests.IAppStoreUpgradeOperation;
|
|
type TAppStoreUpgradeStep = interfaces.requests.TAppStoreUpgradeStep;
|
|
|
|
export class AppStoreHandler {
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
private upgradeOperations = new Map<string, IAppStoreUpgradeOperation>();
|
|
|
|
constructor(private opsServerRef: OpsServer) {
|
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
this.registerHandlers();
|
|
}
|
|
|
|
private getUpgradeOperations(): IAppStoreUpgradeOperation[] {
|
|
return Array.from(this.upgradeOperations.values())
|
|
.sort((a, b) => b.startedAt - a.startedAt)
|
|
.slice(0, 25);
|
|
}
|
|
|
|
private getRunningUpgrade(serviceNameArg: string): IAppStoreUpgradeOperation | null {
|
|
for (const operation of this.upgradeOperations.values()) {
|
|
if (operation.serviceName === serviceNameArg && operation.status === 'running') {
|
|
return operation;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private async createUpgradeOperation(
|
|
serviceNameArg: string,
|
|
targetVersionArg: string,
|
|
): Promise<IAppStoreUpgradeOperation> {
|
|
const existingRunning = this.getRunningUpgrade(serviceNameArg);
|
|
if (existingRunning) {
|
|
throw new plugins.typedrequest.TypedResponseError(
|
|
`An upgrade is already running for ${serviceNameArg}`,
|
|
);
|
|
}
|
|
|
|
const existingService = this.opsServerRef.oneboxRef.database.getServiceByName(serviceNameArg);
|
|
if (!existingService) {
|
|
throw new plugins.typedrequest.TypedResponseError(`Service not found: ${serviceNameArg}`);
|
|
}
|
|
if (!existingService.appTemplateId) {
|
|
throw new plugins.typedrequest.TypedResponseError('Service was not deployed from an app template');
|
|
}
|
|
if (!existingService.appTemplateVersion) {
|
|
throw new plugins.typedrequest.TypedResponseError('Service has no tracked template version');
|
|
}
|
|
|
|
const now = Date.now();
|
|
const operation: IAppStoreUpgradeOperation = {
|
|
id: crypto.randomUUID(),
|
|
serviceName: existingService.name,
|
|
appTemplateId: existingService.appTemplateId,
|
|
fromVersion: existingService.appTemplateVersion,
|
|
targetVersion: targetVersionArg,
|
|
status: 'running',
|
|
step: 'queued',
|
|
progressLines: [`Queued upgrade ${existingService.appTemplateVersion} -> ${targetVersionArg}`],
|
|
warnings: [],
|
|
startedAt: now,
|
|
updatedAt: now,
|
|
};
|
|
|
|
this.upgradeOperations.set(operation.id, operation);
|
|
await this.pushUpgradeProgress(operation);
|
|
return operation;
|
|
}
|
|
|
|
private async updateUpgradeOperation(
|
|
operationIdArg: string,
|
|
stepArg: TAppStoreUpgradeStep,
|
|
messageArg: string,
|
|
updatesArg: Partial<IAppStoreUpgradeOperation> = {},
|
|
): Promise<IAppStoreUpgradeOperation> {
|
|
const existing = this.upgradeOperations.get(operationIdArg);
|
|
if (!existing) {
|
|
throw new Error(`Upgrade operation not found: ${operationIdArg}`);
|
|
}
|
|
|
|
const nextOperation: IAppStoreUpgradeOperation = {
|
|
...existing,
|
|
...updatesArg,
|
|
step: stepArg,
|
|
updatedAt: Date.now(),
|
|
progressLines: [...existing.progressLines, messageArg].slice(-200),
|
|
};
|
|
this.upgradeOperations.set(operationIdArg, nextOperation);
|
|
await this.pushUpgradeProgress(nextOperation);
|
|
return nextOperation;
|
|
}
|
|
|
|
private async pushUpgradeProgress(operationArg: IAppStoreUpgradeOperation): Promise<void> {
|
|
await this.opsServerRef.pushDashboardEvent('pushAppStoreUpgradeProgress', {
|
|
operation: operationArg,
|
|
});
|
|
}
|
|
|
|
private async performUpgrade(operationIdArg: string): Promise<interfaces.data.IService> {
|
|
let operation = this.upgradeOperations.get(operationIdArg);
|
|
if (!operation) {
|
|
throw new Error(`Upgrade operation not found: ${operationIdArg}`);
|
|
}
|
|
|
|
try {
|
|
operation = await this.updateUpgradeOperation(
|
|
operation.id,
|
|
'validating',
|
|
`Validating ${operation.serviceName} for App Store upgrade`,
|
|
);
|
|
|
|
const existingService = this.opsServerRef.oneboxRef.database.getServiceByName(operation.serviceName);
|
|
if (!existingService) {
|
|
throw new Error(`Service not found: ${operation.serviceName}`);
|
|
}
|
|
if (!existingService.appTemplateId || !existingService.appTemplateVersion) {
|
|
throw new Error('Service is missing App Store template metadata');
|
|
}
|
|
|
|
logger.info(
|
|
`Upgrading service '${operation.serviceName}' from v${operation.fromVersion} to v${operation.targetVersion}`,
|
|
);
|
|
|
|
await this.updateUpgradeOperation(
|
|
operation.id,
|
|
'migration',
|
|
`Resolving migration for ${operation.appTemplateId} ${operation.fromVersion} -> ${operation.targetVersion}`,
|
|
);
|
|
|
|
const migrationResult = await this.opsServerRef.oneboxRef.appStore.executeMigration(
|
|
existingService,
|
|
operation.fromVersion,
|
|
operation.targetVersion,
|
|
);
|
|
|
|
if (!migrationResult.success) {
|
|
throw new Error(`Migration failed: ${migrationResult.warnings.join('; ')}`);
|
|
}
|
|
|
|
if (migrationResult.warnings.length > 0) {
|
|
operation = await this.updateUpgradeOperation(
|
|
operation.id,
|
|
'migration',
|
|
`Migration completed with ${migrationResult.warnings.length} warning(s)`,
|
|
{ warnings: migrationResult.warnings },
|
|
);
|
|
}
|
|
|
|
await this.updateUpgradeOperation(
|
|
operation.id,
|
|
'applying',
|
|
`Applying upgrade to ${operation.serviceName}`,
|
|
);
|
|
|
|
const updatedService = await this.opsServerRef.oneboxRef.appStore.applyUpgrade(
|
|
operation.serviceName,
|
|
migrationResult,
|
|
operation.targetVersion,
|
|
{
|
|
onProgress: async (progressArg) => {
|
|
await this.updateUpgradeOperation(
|
|
operation!.id,
|
|
progressArg.step as TAppStoreUpgradeStep,
|
|
progressArg.message,
|
|
);
|
|
},
|
|
},
|
|
);
|
|
|
|
await this.updateUpgradeOperation(
|
|
operation.id,
|
|
'complete',
|
|
`Upgrade completed for ${operation.serviceName}`,
|
|
{
|
|
status: 'success',
|
|
completedAt: Date.now(),
|
|
service: updatedService,
|
|
warnings: migrationResult.warnings,
|
|
},
|
|
);
|
|
|
|
return updatedService;
|
|
} catch (error) {
|
|
await this.updateUpgradeOperation(
|
|
operation.id,
|
|
'failed',
|
|
`Upgrade failed: ${getErrorMessage(error)}`,
|
|
{
|
|
status: 'failed',
|
|
completedAt: Date.now(),
|
|
error: getErrorMessage(error),
|
|
},
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private registerHandlers(): void {
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAppStoreTemplates>(
|
|
'getAppStoreTemplates',
|
|
async (dataArg) => {
|
|
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
|
const apps = await this.opsServerRef.oneboxRef.appStore.getApps();
|
|
return { apps };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAppStoreConfig>(
|
|
'getAppStoreConfig',
|
|
async (dataArg) => {
|
|
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
|
const config = await this.opsServerRef.oneboxRef.appStore.getAppVersionConfig(
|
|
dataArg.appId,
|
|
dataArg.version,
|
|
);
|
|
const appMeta = await this.opsServerRef.oneboxRef.appStore.getAppMeta(dataArg.appId);
|
|
return { config, appMeta };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_InstallAppStoreApp>(
|
|
'installAppStoreApp',
|
|
async (dataArg) => {
|
|
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
|
const service = await this.opsServerRef.oneboxRef.appStore.installApp(dataArg.install);
|
|
return { service };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetUpgradeableAppStoreServices>(
|
|
'getUpgradeableAppStoreServices',
|
|
async (dataArg) => {
|
|
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
|
const services = await this.opsServerRef.oneboxRef.appStore.getUpgradeableAppStoreServices();
|
|
return { services };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpgradeAppStoreService>(
|
|
'upgradeAppStoreService',
|
|
async (dataArg) => {
|
|
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
|
const operation = await this.createUpgradeOperation(dataArg.serviceName, dataArg.targetVersion);
|
|
const updatedService = await this.performUpgrade(operation.id);
|
|
const completedOperation = this.upgradeOperations.get(operation.id)!;
|
|
|
|
return {
|
|
service: updatedService,
|
|
warnings: completedOperation.warnings,
|
|
};
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_StartAppStoreServiceUpgrade>(
|
|
'startAppStoreServiceUpgrade',
|
|
async (dataArg) => {
|
|
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
|
const operation = await this.createUpgradeOperation(dataArg.serviceName, dataArg.targetVersion);
|
|
void this.performUpgrade(operation.id).catch(() => {});
|
|
return { operation };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAppStoreUpgradeOperations>(
|
|
'getAppStoreUpgradeOperations',
|
|
async (dataArg) => {
|
|
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
|
return { operations: this.getUpgradeOperations() };
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|