chore: update cloudly dependency stack
Align Cloudly with the current typedserver, smartconfig, smartstate, and Docker tooling releases so builds and Docker output stay compatible with the upgraded stack.
This commit is contained in:
@@ -82,7 +82,7 @@ export class Cloudly {
|
||||
|
||||
private readyDeferred = new plugins.smartpromise.Deferred();
|
||||
|
||||
private configOptions: plugins.servezoneInterfaces.data.ICloudlyConfig;
|
||||
private configOptions?: plugins.servezoneInterfaces.data.ICloudlyConfig;
|
||||
constructor(configArg?: plugins.servezoneInterfaces.data.ICloudlyConfig) {
|
||||
this.configOptions = configArg;
|
||||
this.cloudlyInfo = new CloudlyInfo(this);
|
||||
@@ -148,7 +148,9 @@ export class Cloudly {
|
||||
await this.domainManager.init();
|
||||
|
||||
await this.cloudflareConnector.init();
|
||||
await this.letsencryptConnector.init();
|
||||
if (this.config.data.sslMode === 'letsencrypt') {
|
||||
await this.letsencryptConnector.init();
|
||||
}
|
||||
await this.clusterManager.init();
|
||||
await this.server.start();
|
||||
this.readyDeferred.resolve();
|
||||
@@ -163,7 +165,9 @@ export class Cloudly {
|
||||
*/
|
||||
public async stop() {
|
||||
await this.server.stop();
|
||||
await this.letsencryptConnector.stop();
|
||||
if (this.config.data.sslMode === 'letsencrypt') {
|
||||
await this.letsencryptConnector.stop();
|
||||
}
|
||||
await this.mongodbConnector.stop();
|
||||
await this.secretManager.stop();
|
||||
await this.serviceManager.stop();
|
||||
|
||||
@@ -9,5 +9,5 @@ export class CloudlyInfo {
|
||||
this.cloudlyRef = cloudlyRefArg;
|
||||
}
|
||||
|
||||
public projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||
public projectInfo = plugins.projectinfo.ProjectInfo.create(paths.packageDir);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import type { Cloudly } from './classes.cloudly.js';
|
||||
*/
|
||||
export class CloudlyConfig {
|
||||
public cloudlyRef: Cloudly;
|
||||
public appData: plugins.npmextra.AppData<plugins.servezoneInterfaces.data.ICloudlyConfig>;
|
||||
public data: plugins.servezoneInterfaces.data.ICloudlyConfig;
|
||||
public appData!: plugins.smartconfig.AppData<plugins.servezoneInterfaces.data.ICloudlyConfig>;
|
||||
public data!: plugins.servezoneInterfaces.data.ICloudlyConfig;
|
||||
|
||||
constructor(cloudlyRefArg: Cloudly) {
|
||||
this.cloudlyRef = cloudlyRefArg;
|
||||
@@ -17,12 +17,12 @@ export class CloudlyConfig {
|
||||
|
||||
public async init(configArg?: plugins.servezoneInterfaces.data.ICloudlyConfig) {
|
||||
this.appData =
|
||||
await plugins.npmextra.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>(
|
||||
await plugins.smartconfig.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>(
|
||||
{
|
||||
envMapping: {
|
||||
environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration',
|
||||
letsEncryptEmail: 'hard:domains@lossless.org',
|
||||
letsEncryptPrivateKey: null,
|
||||
letsEncryptPrivateKey: undefined,
|
||||
publicUrl: 'SERVEZONE_URL',
|
||||
publicPort: 'SERVEZONE_PORT',
|
||||
mongoDescriptor: {
|
||||
@@ -50,6 +50,7 @@ export class CloudlyConfig {
|
||||
'sslMode',
|
||||
'environment',
|
||||
'mongoDescriptor',
|
||||
's3Descriptor',
|
||||
],
|
||||
overwriteObject: configArg,
|
||||
},
|
||||
|
||||
+30
-48
@@ -11,13 +11,13 @@ export class CloudlyServer {
|
||||
* a reference to the cloudly instance
|
||||
*/
|
||||
public cloudlyRef: Cloudly;
|
||||
public additionalHandlers: plugins.typedserver.servertools.Handler[] = [];
|
||||
public additionalHandlers: plugins.typedserver.IRouteHandler[] = [];
|
||||
|
||||
/**
|
||||
* the smartexpress server handling the actual requests
|
||||
*/
|
||||
public typedServer: plugins.typedserver.TypedServer;
|
||||
public typedsocketServer: plugins.typedsocket.TypedSocket;
|
||||
public typedServer!: plugins.typedserver.TypedServer;
|
||||
public typedsocketServer!: plugins.typedsocket.TypedSocket;
|
||||
|
||||
/**
|
||||
* typedrouter
|
||||
@@ -39,13 +39,13 @@ export class CloudlyServer {
|
||||
*/
|
||||
public async start() {
|
||||
logger.log('info', `cloudly domain is ${this.cloudlyRef.config.data.publicUrl}`);
|
||||
let sslCert: plugins.smartacme.Cert;
|
||||
let sslCert: plugins.smartacme.Cert | undefined;
|
||||
|
||||
if (this.cloudlyRef.config.data.sslMode === 'letsencrypt') {
|
||||
logger.log('info', `Using letsencrypt for ssl mode. Trying to obtain a certificate...`);
|
||||
logger.log('info', `This might take 10 minutes...`);
|
||||
sslCert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
|
||||
this.cloudlyRef.config.data.publicUrl,
|
||||
this.cloudlyRef.config.data.publicUrl!,
|
||||
);
|
||||
logger.log(
|
||||
'success',
|
||||
@@ -58,23 +58,6 @@ export class CloudlyServer {
|
||||
);
|
||||
}
|
||||
|
||||
interface IRequestGuardData {
|
||||
req: plugins.typedserver.Request;
|
||||
res: plugins.typedserver.Response;
|
||||
}
|
||||
|
||||
// guards
|
||||
const guardIp = new plugins.smartguard.Guard<IRequestGuardData>(async (dataArg) => {
|
||||
if (true) {
|
||||
return true;
|
||||
} else {
|
||||
dataArg.res.status(500);
|
||||
dataArg.res.send(`Not allowed to perform this operation!`);
|
||||
dataArg.res.end();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// server
|
||||
this.typedServer = new plugins.typedserver.TypedServer({
|
||||
cors: true,
|
||||
@@ -89,43 +72,42 @@ export class CloudlyServer {
|
||||
injectReload: true,
|
||||
serveDir: paths.distServeDir,
|
||||
watch: true,
|
||||
enableCompression: true,
|
||||
preferredCompressionMethod: 'gzip',
|
||||
compression: {
|
||||
enabled: true,
|
||||
algorithms: ['gzip'],
|
||||
},
|
||||
});
|
||||
this.typedsocketServer = this.typedServer.typedsocket;
|
||||
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.typedServer.server.addRoute(
|
||||
this.typedServer.addRoute(
|
||||
'/v2',
|
||||
new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
|
||||
await this.cloudlyRef.registryManager.handleHttpRequest(req, res);
|
||||
}),
|
||||
'ALL',
|
||||
async (ctx) => this.cloudlyRef.registryManager.handleHttpRequest(ctx),
|
||||
);
|
||||
this.typedServer.server.addRoute(
|
||||
'/v2/{*splat}',
|
||||
new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
|
||||
await this.cloudlyRef.registryManager.handleHttpRequest(req, res);
|
||||
}),
|
||||
this.typedServer.addRoute(
|
||||
'/v2/*',
|
||||
'ALL',
|
||||
async (ctx) => this.cloudlyRef.registryManager.handleHttpRequest(ctx),
|
||||
);
|
||||
this.typedServer.server.addRoute(
|
||||
this.typedServer.addRoute(
|
||||
'/curlfresh/:scriptname',
|
||||
this.cloudlyRef.nodeManager.curlfreshInstance.handler,
|
||||
'ALL',
|
||||
async (ctx) => this.cloudlyRef.nodeManager.curlfreshInstance.handleRequest(ctx),
|
||||
);
|
||||
this.typedServer.server.addRoute(
|
||||
this.typedServer.addRoute(
|
||||
'/baseos/v1/nodes/register',
|
||||
new plugins.typedserver.servertools.Handler('POST', async (req, res) => {
|
||||
await this.cloudlyRef.baseOsManager.handleRegisterHttpRequest(req, res);
|
||||
}),
|
||||
'POST',
|
||||
async (ctx) => this.cloudlyRef.baseOsManager.handleRegisterHttpRequest(ctx),
|
||||
);
|
||||
this.typedServer.server.addRoute(
|
||||
this.typedServer.addRoute(
|
||||
'/baseos/v1/nodes/heartbeat',
|
||||
new plugins.typedserver.servertools.Handler('POST', async (req, res) => {
|
||||
await this.cloudlyRef.baseOsManager.handleHeartbeatHttpRequest(req, res);
|
||||
}),
|
||||
'POST',
|
||||
async (ctx) => this.cloudlyRef.baseOsManager.handleHeartbeatHttpRequest(ctx),
|
||||
);
|
||||
this.typedServer.server.addRoute(
|
||||
this.typedServer.addRoute(
|
||||
'/baseos/v1/images/:buildId/download',
|
||||
new plugins.typedserver.servertools.Handler('GET', async (req, res) => {
|
||||
await this.cloudlyRef.baseOsManager.handleImageDownloadHttpRequest(req, res);
|
||||
}),
|
||||
'GET',
|
||||
async (ctx) => this.cloudlyRef.baseOsManager.handleImageDownloadHttpRequest(ctx),
|
||||
);
|
||||
await this.typedServer.start();
|
||||
}
|
||||
@@ -134,6 +116,6 @@ export class CloudlyServer {
|
||||
* stop the reception instance
|
||||
*/
|
||||
public async stop() {
|
||||
await this.typedServer.stop();
|
||||
await this.typedServer?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Cloudly } from '../classes.cloudly.js';
|
||||
*/
|
||||
export class CloudflareConnector {
|
||||
private cloudlyRef: Cloudly;
|
||||
public cloudflare: plugins.cloudflare.CloudflareAccount;
|
||||
public cloudflare?: plugins.cloudflare.CloudflareAccount;
|
||||
|
||||
constructor(cloudlyArg: Cloudly) {
|
||||
this.cloudlyRef = cloudlyArg;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Cloudly } from '../classes.cloudly.js';
|
||||
|
||||
export class LetsencryptConnector {
|
||||
private cloudlyRef: Cloudly;
|
||||
private smartacme: plugins.smartacme.SmartAcme;
|
||||
private smartacme!: plugins.smartacme.SmartAcme;
|
||||
|
||||
constructor(cloudlyArg: Cloudly) {
|
||||
this.cloudlyRef = cloudlyArg;
|
||||
@@ -18,6 +18,10 @@ export class LetsencryptConnector {
|
||||
* inits letsencrypt
|
||||
*/
|
||||
public async init() {
|
||||
if (!this.cloudlyRef.cloudflareConnector.cloudflare) {
|
||||
throw new Error('Cloudflare token is required for letsencrypt DNS-01 challenges');
|
||||
}
|
||||
|
||||
// Create DNS-01 challenge handler using Cloudflare
|
||||
const dnsHandler = new plugins.smartacme.handlers.Dns01Handler(
|
||||
this.cloudlyRef.cloudflareConnector.cloudflare
|
||||
@@ -25,13 +29,13 @@ export class LetsencryptConnector {
|
||||
|
||||
// Create MongoDB certificate manager
|
||||
const certManager = new plugins.smartacme.certmanagers.MongoCertManager(
|
||||
this.cloudlyRef.config.data.mongoDescriptor
|
||||
this.cloudlyRef.config.data.mongoDescriptor!
|
||||
);
|
||||
|
||||
this.smartacme = new plugins.smartacme.SmartAcme({
|
||||
accountEmail: this.cloudlyRef.config.data.letsEncryptEmail,
|
||||
accountEmail: this.cloudlyRef.config.data.letsEncryptEmail!,
|
||||
accountPrivateKey: this.cloudlyRef.config.data.letsEncryptPrivateKey,
|
||||
environment: this.cloudlyRef.config.data.environment,
|
||||
environment: this.cloudlyRef.config.data.environment!,
|
||||
certManager: certManager,
|
||||
challengeHandlers: [dnsHandler],
|
||||
});
|
||||
@@ -45,6 +49,6 @@ export class LetsencryptConnector {
|
||||
* stops the instance
|
||||
*/
|
||||
public async stop() {
|
||||
await this.smartacme.stop();
|
||||
await this.smartacme?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Cloudly } from '../classes.cloudly.js';
|
||||
export class MongodbConnector {
|
||||
// INSTANCE
|
||||
private cloudlyRef: Cloudly;
|
||||
public smartdataDb: plugins.smartdata.SmartdataDb;
|
||||
public smartdataDb!: plugins.smartdata.SmartdataDb;
|
||||
|
||||
constructor(cloudlyRefArg: Cloudly) {
|
||||
this.cloudlyRef = cloudlyRefArg;
|
||||
@@ -12,7 +12,7 @@ export class MongodbConnector {
|
||||
|
||||
public async init() {
|
||||
this.smartdataDb = new plugins.smartdata.SmartdataDb(
|
||||
this.cloudlyRef.config.data.mongoDescriptor,
|
||||
this.cloudlyRef.config.data.mongoDescriptor!,
|
||||
);
|
||||
await this.smartdataDb.init();
|
||||
}
|
||||
|
||||
+6
-6
@@ -3,12 +3,12 @@ import * as paths from './paths.js';
|
||||
|
||||
export const logger = new plugins.smartlog.Smartlog({
|
||||
logContext: {
|
||||
company: null,
|
||||
environment: null,
|
||||
runtime: null,
|
||||
zone: null,
|
||||
companyunit: null,
|
||||
containerName: null,
|
||||
company: undefined,
|
||||
environment: undefined,
|
||||
runtime: undefined,
|
||||
zone: undefined,
|
||||
companyunit: undefined,
|
||||
containerName: undefined,
|
||||
},
|
||||
});
|
||||
logger.enableConsole({
|
||||
|
||||
@@ -20,7 +20,7 @@ export class CloudlyAuthManager {
|
||||
public CAuthorization = plugins.smartdata.setDefaultManagerForDoc(this, Authorization);
|
||||
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
public smartjwtInstance: plugins.smartjwt.SmartJwt<IJwtData>;
|
||||
public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
|
||||
|
||||
constructor(cloudlyRef: Cloudly) {
|
||||
this.cloudlyRef = cloudlyRef;
|
||||
@@ -73,7 +73,7 @@ export class CloudlyAuthManager {
|
||||
identity: {
|
||||
jwt,
|
||||
userId: user.id,
|
||||
name: user.data.username,
|
||||
name: user.data.username || user.id,
|
||||
expiresAt: expiresAtTimestamp,
|
||||
role: user.data.role,
|
||||
type: user.data.type,
|
||||
|
||||
@@ -45,8 +45,8 @@ export class User extends plugins.smartdata.SmartDataDbDoc<
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.servezoneInterfaces.data.IUser['data'];
|
||||
public data!: plugins.servezoneInterfaces.data.IUser['data'];
|
||||
}
|
||||
|
||||
@@ -256,17 +256,19 @@ export class CloudlyBackupManager {
|
||||
backup.tags = requestArg.tags;
|
||||
await backup.save();
|
||||
|
||||
const replicationEnabled = (requestArg as any).replicate !== false && !!process.env.CLOUDLY_BACKUP_TARGET_TYPE;
|
||||
|
||||
try {
|
||||
const result = await this.fireCoreflowRequest('executeServiceBackup', {
|
||||
backupId: backup.id,
|
||||
service: await service.createSavableObject(),
|
||||
tags: requestArg.tags,
|
||||
replication: {
|
||||
enabled: true,
|
||||
enabled: replicationEnabled,
|
||||
},
|
||||
}, backup.clusterId);
|
||||
backup.snapshots = result.snapshots || [];
|
||||
if (!result.replication) {
|
||||
if (replicationEnabled && !result.replication) {
|
||||
throw new Error('Coreflow did not complete remote backup replication');
|
||||
}
|
||||
backup.replication = result.replication;
|
||||
|
||||
@@ -67,7 +67,10 @@ class S3BackupTargetWriter implements IBackupTargetWriter {
|
||||
: {}),
|
||||
} as any);
|
||||
const bucketName = requiredEnv('CLOUDLY_BACKUP_S3_BUCKET');
|
||||
return await smartBucket.getBucketByName(bucketName) || await smartBucket.createBucket(bucketName);
|
||||
if (await smartBucket.bucketExists(bucketName)) {
|
||||
return await smartBucket.getBucketByName(bucketName);
|
||||
}
|
||||
return await smartBucket.createBucket(bucketName);
|
||||
})();
|
||||
}
|
||||
return await this.bucketPromise;
|
||||
|
||||
@@ -12,29 +12,38 @@ export class BareMetal extends plugins.smartdata.SmartDataDbDoc<
|
||||
public static async createFromHetznerServer(
|
||||
hetznerServerArg: plugins.hetznercloud.HetznerServer,
|
||||
) {
|
||||
const serverData = hetznerServerArg.data;
|
||||
if (!serverData) {
|
||||
throw new Error('Hetzner server response is missing server data');
|
||||
}
|
||||
const ipv4 = serverData.public_net.ipv4;
|
||||
if (!ipv4) {
|
||||
throw new Error(`Hetzner server ${serverData.id} has no primary IPv4 address`);
|
||||
}
|
||||
|
||||
const newBareMetal = new BareMetal();
|
||||
newBareMetal.id = plugins.smartunique.shortId(8);
|
||||
const data: plugins.servezoneInterfaces.data.IBareMetal['data'] = {
|
||||
hostname: hetznerServerArg.data.name,
|
||||
primaryIp: hetznerServerArg.data.public_net.ipv4.ip,
|
||||
hostname: serverData.name,
|
||||
primaryIp: ipv4.ip,
|
||||
provider: 'hetzner',
|
||||
location: hetznerServerArg.data.datacenter.name,
|
||||
location: serverData.datacenter.name,
|
||||
specs: {
|
||||
cpuModel: hetznerServerArg.data.server_type.cpu_type,
|
||||
cpuCores: hetznerServerArg.data.server_type.cores,
|
||||
memoryGB: hetznerServerArg.data.server_type.memory,
|
||||
storageGB: hetznerServerArg.data.server_type.disk,
|
||||
cpuModel: serverData.server_type.cpu_type,
|
||||
cpuCores: serverData.server_type.cores,
|
||||
memoryGB: serverData.server_type.memory,
|
||||
storageGB: serverData.server_type.disk,
|
||||
storageType: 'nvme',
|
||||
},
|
||||
powerState: hetznerServerArg.data.status === 'running' ? 'on' : 'off',
|
||||
powerState: serverData.status === 'running' ? 'on' : 'off',
|
||||
osInfo: {
|
||||
name: 'Debian',
|
||||
version: '12',
|
||||
},
|
||||
assignedNodeIds: [],
|
||||
providerMetadata: {
|
||||
hetznerServerId: hetznerServerArg.data.id,
|
||||
hetznerServerName: hetznerServerArg.data.name,
|
||||
hetznerServerId: serverData.id,
|
||||
hetznerServerName: serverData.name,
|
||||
},
|
||||
};
|
||||
Object.assign(newBareMetal, { data });
|
||||
@@ -44,10 +53,10 @@ export class BareMetal extends plugins.smartdata.SmartDataDbDoc<
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.servezoneInterfaces.data.IBareMetal['data'];
|
||||
public data!: plugins.servezoneInterfaces.data.IBareMetal['data'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -101,4 +110,4 @@ export class BareMetal extends plugins.smartdata.SmartDataDbDoc<
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export class CloudlyBaremetalManager {
|
||||
public cloudlyRef: Cloudly;
|
||||
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
|
||||
public hetznerAccount?: plugins.hetznercloud.HetznerAccount;
|
||||
|
||||
public get db() {
|
||||
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||
@@ -119,18 +119,22 @@ export class CloudlyBaremetalManager {
|
||||
* Create baremetal from Hetzner server
|
||||
*/
|
||||
public async createBaremetalFromHetznerServer(hetznerServer: plugins.hetznercloud.HetznerServer): Promise<BareMetal> {
|
||||
const serverData = hetznerServer.data;
|
||||
if (!serverData) {
|
||||
throw new Error('Hetzner server response is missing server data');
|
||||
}
|
||||
// Check if baremetal already exists for this Hetzner server
|
||||
const existingBaremetals = await this.CBareMetal.getInstances({});
|
||||
for (const baremetal of existingBaremetals) {
|
||||
if (baremetal.data.providerMetadata?.hetznerServerId === hetznerServer.data.id) {
|
||||
logger.log('info', `BareMetal already exists for Hetzner server ${hetznerServer.data.id}`);
|
||||
if (baremetal.data.providerMetadata?.hetznerServerId === serverData.id) {
|
||||
logger.log('info', `BareMetal already exists for Hetzner server ${serverData.id}`);
|
||||
return baremetal;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new baremetal
|
||||
const newBaremetal = await BareMetal.createFromHetznerServer(hetznerServer);
|
||||
logger.log('success', `Created new BareMetal ${newBaremetal.id} from Hetzner server ${hetznerServer.data.id}`);
|
||||
logger.log('success', `Created new BareMetal ${newBaremetal.id} from Hetzner server ${serverData.id}`);
|
||||
return newBaremetal;
|
||||
}
|
||||
|
||||
@@ -173,4 +177,4 @@ export class CloudlyBaremetalManager {
|
||||
|
||||
throw new Error(`Provider ${options.provider} not supported or not configured`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,15 +305,14 @@ export class CloudlyBaseOsManager {
|
||||
}
|
||||
|
||||
public async handleRegisterHttpRequest(
|
||||
reqArg: plugins.typedserver.Request,
|
||||
resArg: plugins.typedserver.Response,
|
||||
) {
|
||||
ctxArg: plugins.typedserver.IRequestContext,
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const requestData = await this.readJsonBody<IBaseOsRegisterRequest>(reqArg);
|
||||
const requestData = await this.readJsonBody<IBaseOsRegisterRequest>(ctxArg);
|
||||
const response = await this.registerNode(requestData);
|
||||
this.sendJson(resArg, 200, response);
|
||||
return this.createJsonResponse(200, response);
|
||||
} catch (error) {
|
||||
this.sendJson(resArg, 400, {
|
||||
return this.createJsonResponse(400, {
|
||||
accepted: false,
|
||||
message: `BaseOS registration failed: ${(error as Error).message}`,
|
||||
} satisfies IBaseOsRegisterResponse);
|
||||
@@ -321,15 +320,14 @@ export class CloudlyBaseOsManager {
|
||||
}
|
||||
|
||||
public async handleHeartbeatHttpRequest(
|
||||
reqArg: plugins.typedserver.Request,
|
||||
resArg: plugins.typedserver.Response,
|
||||
) {
|
||||
ctxArg: plugins.typedserver.IRequestContext,
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const requestData = await this.readJsonBody<IBaseOsHeartbeatRequest>(reqArg);
|
||||
const requestData = await this.readJsonBody<IBaseOsHeartbeatRequest>(ctxArg);
|
||||
const response = await this.acceptHeartbeat(requestData);
|
||||
this.sendJson(resArg, 200, response);
|
||||
return this.createJsonResponse(200, response);
|
||||
} catch (error) {
|
||||
this.sendJson(resArg, 400, {
|
||||
return this.createJsonResponse(400, {
|
||||
accepted: false,
|
||||
message: `BaseOS heartbeat failed: ${(error as Error).message}`,
|
||||
} satisfies IBaseOsHeartbeatResponse);
|
||||
@@ -337,37 +335,35 @@ export class CloudlyBaseOsManager {
|
||||
}
|
||||
|
||||
public async handleImageDownloadHttpRequest(
|
||||
reqArg: plugins.typedserver.Request,
|
||||
resArg: plugins.typedserver.Response,
|
||||
) {
|
||||
ctxArg: plugins.typedserver.IRequestContext,
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const requestUrl = new URL((reqArg as any).originalUrl || reqArg.url || '/', 'http://localhost');
|
||||
const buildId = requestUrl.pathname.split('/').at(-2);
|
||||
const token = requestUrl.searchParams.get('token');
|
||||
const buildId = ctxArg.params.buildId || ctxArg.url.pathname.split('/').at(-2);
|
||||
const token = ctxArg.url.searchParams.get('token');
|
||||
if (!buildId || !token) {
|
||||
this.sendJson(resArg, 400, { errorText: 'build id or download token missing' });
|
||||
return;
|
||||
return this.createJsonResponse(400, { errorText: 'build id or download token missing' });
|
||||
}
|
||||
const build = await this.getImageBuildById(buildId);
|
||||
if (build.downloadTokenHash !== this.hashSecret(token) || (build.downloadTokenExpiresAt || 0) < Date.now()) {
|
||||
this.sendJson(resArg, 403, { errorText: 'download token is invalid or expired' });
|
||||
return;
|
||||
return this.createJsonResponse(403, { errorText: 'download token is invalid or expired' });
|
||||
}
|
||||
if (build.data.status !== 'ready' || !build.data.artifact) {
|
||||
this.sendJson(resArg, 409, { errorText: 'image build is not ready' });
|
||||
return;
|
||||
return this.createJsonResponse(409, { errorText: 'image build is not ready' });
|
||||
}
|
||||
|
||||
const artifact = build.data.artifact;
|
||||
const bucket = await this.getArtifactBucket(artifact.bucketName);
|
||||
const artifactStream = await bucket.fastGetStream({ path: artifact.key }, 'nodestream');
|
||||
resArg.status(200);
|
||||
resArg.setHeader('Content-Type', artifact.contentType || 'application/octet-stream');
|
||||
resArg.setHeader('Content-Length', String(artifact.size));
|
||||
resArg.setHeader('Content-Disposition', `attachment; filename="${artifact.filename}"`);
|
||||
(artifactStream as nodeStream.Readable).pipe(resArg as any);
|
||||
return new Response(nodeStream.Readable.toWeb(artifactStream as nodeStream.Readable) as ReadableStream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': artifact.contentType || 'application/octet-stream',
|
||||
'Content-Length': String(artifact.size),
|
||||
'Content-Disposition': `attachment; filename="${artifact.filename}"`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.sendJson(resArg, 500, {
|
||||
return this.createJsonResponse(500, {
|
||||
errorText: `BaseOS image download failed: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
@@ -986,22 +982,20 @@ export class CloudlyBaseOsManager {
|
||||
&& typeof runtimeInfo.checkedAt === 'number';
|
||||
}
|
||||
|
||||
private async readJsonBody<T>(reqArg: plugins.typedserver.Request): Promise<T> {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of reqArg as any) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
}
|
||||
const bodyString = Buffer.concat(chunks).toString('utf8').trim();
|
||||
private async readJsonBody<T>(ctxArg: plugins.typedserver.IRequestContext): Promise<T> {
|
||||
const bodyString = (await ctxArg.text()).trim();
|
||||
return bodyString ? JSON.parse(bodyString) as T : {} as T;
|
||||
}
|
||||
|
||||
private sendJson(
|
||||
resArg: plugins.typedserver.Response,
|
||||
private createJsonResponse(
|
||||
statusCodeArg: number,
|
||||
bodyArg: object,
|
||||
) {
|
||||
resArg.status(statusCodeArg);
|
||||
resArg.setHeader('Content-Type', 'application/json');
|
||||
resArg.end(JSON.stringify(bodyArg));
|
||||
): Response {
|
||||
return new Response(JSON.stringify(bodyArg), {
|
||||
status: statusCodeArg,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ export class Cluster extends plugins.smartdata.SmartDataDbDoc<
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.servezoneInterfaces.data.ICluster['data'];
|
||||
public data!: plugins.servezoneInterfaces.data.ICluster['data'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -22,16 +22,19 @@ export class ClusterManager {
|
||||
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
|
||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(
|
||||
new plugins.typedrequest.TypedHandler('createCluster', async (dataArg) => {
|
||||
// TODO: guards
|
||||
new plugins.typedrequest.TypedHandler('createCluster', async (dataArg, toolsArg) => {
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||
const setupMode = dataArg.setupMode || 'manual'; // Default to manual if not specified
|
||||
const cluster = await this.createCluster({
|
||||
id: plugins.smartunique.uniSimple('cluster'),
|
||||
data: {
|
||||
userId: null, // this is created by the createCluster method
|
||||
userId: '', // this is created by the createCluster method
|
||||
name: dataArg.clusterName,
|
||||
setupMode: setupMode,
|
||||
acmeInfo: null,
|
||||
acmeInfo: {
|
||||
serverAddress: '',
|
||||
serverSecret: '',
|
||||
},
|
||||
cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`,
|
||||
nodes: [],
|
||||
sshKeys: [],
|
||||
@@ -51,8 +54,8 @@ export class ClusterManager {
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_GetClusters>(
|
||||
new plugins.typedrequest.TypedHandler('getClusters', async (dataArg) => {
|
||||
// TODO: do authentication here
|
||||
new plugins.typedrequest.TypedHandler('getClusters', async (dataArg, toolsArg) => {
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||
const clusters = await this.getAllClusters();
|
||||
return {
|
||||
clusters: await Promise.all(
|
||||
@@ -62,10 +65,41 @@ export class ClusterManager {
|
||||
}),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_GetClusterById>(
|
||||
new plugins.typedrequest.TypedHandler('getClusterById', async (dataArg, toolsArg) => {
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||
const cluster = await this.CCluster.getInstance({ id: (dataArg as any).clusterId });
|
||||
if (!cluster) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Cluster not found');
|
||||
}
|
||||
return {
|
||||
cluster: await cluster.createSavableObject(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_UpdateCluster>(
|
||||
new plugins.typedrequest.TypedHandler('updateCluster', async (dataArg, toolsArg) => {
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||
const cluster = await this.CCluster.getInstance({ id: (dataArg as any).clusterId });
|
||||
if (!cluster) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Cluster not found');
|
||||
}
|
||||
cluster.data = {
|
||||
...cluster.data,
|
||||
...dataArg.clusterData,
|
||||
};
|
||||
await cluster.save();
|
||||
return {
|
||||
resultCluster: await cluster.createSavableObject(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// delete cluster
|
||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_DeleteClusterById>(
|
||||
new plugins.typedrequest.TypedHandler('deleteClusterById', async (reqDataArg, toolsArg) => {
|
||||
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqDataArg);
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqDataArg);
|
||||
await this.deleteCluster(reqDataArg.clusterId);
|
||||
return {
|
||||
ok: true,
|
||||
@@ -134,7 +168,6 @@ export class ClusterManager {
|
||||
* @param configObjectArg
|
||||
*/
|
||||
public async createCluster(configObjectArg: plugins.servezoneInterfaces.data.ICluster) {
|
||||
// TODO: guards
|
||||
// lets create the cluster user
|
||||
const clusterUser = new this.cloudlyRef.authManager.CUser({
|
||||
id: await this.cloudlyRef.authManager.CUser.getNewId(),
|
||||
|
||||
@@ -9,28 +9,28 @@ export class Deployment extends plugins.smartdata.SmartDataDbDoc<
|
||||
public id: string = plugins.smartunique.uniSimple('deployment');
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public serviceId: string;
|
||||
public serviceId!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public nodeId: string;
|
||||
public nodeId!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public containerId?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public usedImageId: string;
|
||||
public usedImageId!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public version: string;
|
||||
public version!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public deployedAt: number;
|
||||
public deployedAt!: number;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public deploymentLog: string[] = [];
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public status: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
|
||||
public status!: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
|
||||
@@ -95,4 +95,4 @@ export class Deployment extends plugins.smartdata.SmartDataDbDoc<
|
||||
resourceUsage: this.resourceUsage,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +79,10 @@ export class DnsEntry extends plugins.smartdata.SmartDataDbDoc<
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.svDb()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.servezoneInterfaces.data.IDnsEntry['data'];
|
||||
public data!: plugins.servezoneInterfaces.data.IDnsEntry['data'];
|
||||
|
||||
/**
|
||||
* Validates the DNS entry data
|
||||
@@ -146,4 +146,4 @@ export class DnsEntry extends plugins.smartdata.SmartDataDbDoc<
|
||||
result += ` ${value}`;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +95,10 @@ export class Domain extends plugins.smartdata.SmartDataDbDoc<
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.servezoneInterfaces.data.IDomain['data'];
|
||||
public data!: plugins.servezoneInterfaces.data.IDomain['data'];
|
||||
|
||||
/**
|
||||
* Verify domain ownership
|
||||
|
||||
@@ -107,10 +107,10 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR
|
||||
// INSTANCE
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.servezoneInterfaces.data.IExternalRegistry['data'];
|
||||
public data!: plugins.servezoneInterfaces.data.IExternalRegistry['data'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -170,10 +170,11 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR
|
||||
|
||||
return { success: false, message: 'Unknown registry type' };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.data.status = 'error';
|
||||
this.data.lastError = error.message;
|
||||
this.data.lastError = errorMessage;
|
||||
await this.save();
|
||||
return { success: false, message: error.message };
|
||||
return { success: false, message: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ export class Image extends plugins.smartdata.SmartDataDbDoc<
|
||||
}
|
||||
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.servezoneInterfaces.data.IImage['data'];
|
||||
public data!: plugins.servezoneInterfaces.data.IImage['data'];
|
||||
|
||||
public async getVersions() {}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import { Image } from './classes.image.js';
|
||||
export class ImageManager {
|
||||
cloudlyRef: Cloudly;
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
public smartbucketInstance: plugins.smartbucket.SmartBucket;
|
||||
public imageDir: plugins.smartbucket.Directory;
|
||||
public dockerImageStore: plugins.docker.DockerImageStore;
|
||||
public smartbucketInstance!: plugins.smartbucket.SmartBucket;
|
||||
public imageDir!: plugins.smartbucket.Directory;
|
||||
public dockerImageStore!: plugins.docker.DockerImageStore;
|
||||
|
||||
get db() {
|
||||
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||
@@ -26,7 +26,7 @@ export class ImageManager {
|
||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_CreateImage>(
|
||||
'createImage',
|
||||
async (reqArg, toolsArg) => {
|
||||
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg);
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg);
|
||||
const image = await this.CImage.create({
|
||||
name: reqArg.name,
|
||||
description: reqArg.description,
|
||||
@@ -41,7 +41,7 @@ export class ImageManager {
|
||||
|
||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetImage>(
|
||||
new plugins.typedrequest.TypedHandler('getImage', async (reqArg, toolsArg) => {
|
||||
await toolsArg.passGuards([this.cloudlyRef.authManager.adminOrClusterIdentityGuard], reqArg);
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminOrClusterIdentityGuard], reqArg);
|
||||
const image = await this.CImage.getInstance({
|
||||
id: reqArg.imageId,
|
||||
});
|
||||
@@ -55,7 +55,7 @@ export class ImageManager {
|
||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_DeleteImage>(
|
||||
'deleteImage',
|
||||
async (reqArg, toolsArg) => {
|
||||
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg);
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg);
|
||||
const image = await this.CImage.getInstance({
|
||||
id: reqArg.imageId,
|
||||
});
|
||||
@@ -69,7 +69,7 @@ export class ImageManager {
|
||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetAllImages>(
|
||||
'getAllImages',
|
||||
async (requestArg, toolsArg) => {
|
||||
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], requestArg);
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], requestArg);
|
||||
const images = await this.CImage.getInstances({});
|
||||
return {
|
||||
images: await Promise.all(
|
||||
@@ -96,9 +96,24 @@ export class ImageManager {
|
||||
throw new plugins.typedrequest.TypedResponseError('Image not found');
|
||||
}
|
||||
const imageVersion = reqArg.versionString;
|
||||
if (!imageVersion) {
|
||||
throw new plugins.typedrequest.TypedResponseError('versionString is required');
|
||||
}
|
||||
console.log(
|
||||
`got request to push image version ${imageVersion} for image ${refImage.data.name}`,
|
||||
);
|
||||
const storagePath = await refImage.getStoragePath(imageVersion);
|
||||
refImage.data.versions = [
|
||||
...refImage.data.versions.filter((version) => version.versionString !== imageVersion),
|
||||
{
|
||||
versionString: imageVersion,
|
||||
source: 'upload',
|
||||
storagePath,
|
||||
size: 0,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
await refImage.save();
|
||||
const imagePushStream = reqArg.imageStream;
|
||||
(async () => {
|
||||
const smartWebDuplex = new plugins.smartstream.webstream.WebDuplexStream<
|
||||
@@ -112,10 +127,12 @@ export class ImageManager {
|
||||
});
|
||||
imagePushStream.writeToWebstream(smartWebDuplex.writable);
|
||||
await this.dockerImageStore.storeImage(
|
||||
refImage.id,
|
||||
storagePath,
|
||||
plugins.smartstream.SmartDuplex.fromWebReadableStream(smartWebDuplex.readable),
|
||||
);
|
||||
})();
|
||||
})().catch((error) => {
|
||||
console.error(`failed to store image ${refImage.id}:${imageVersion}`, error);
|
||||
});
|
||||
return {
|
||||
allowed: true,
|
||||
};
|
||||
@@ -133,13 +150,21 @@ export class ImageManager {
|
||||
const imageVersion = image.data.versions.find(
|
||||
(version) => version.versionString === reqArg.versionString,
|
||||
);
|
||||
const readable = this.imageDir.fastGetStream(
|
||||
if (!imageVersion) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Image version not found');
|
||||
}
|
||||
const readable = await this.imageDir.fastGetStream(
|
||||
{
|
||||
path: await image.getStoragePath(reqArg.versionString),
|
||||
path: imageVersion.storagePath || await image.getStoragePath(reqArg.versionString),
|
||||
},
|
||||
'webstream',
|
||||
);
|
||||
const imageVirtualStream = new plugins.typedrequest.VirtualStream();
|
||||
(async () => {
|
||||
await imageVirtualStream.readFromWebstream(readable);
|
||||
})().catch((error) => {
|
||||
console.error(`failed to stream image ${image.id}:${reqArg.versionString}`, error);
|
||||
});
|
||||
return {
|
||||
imageStream: imageVirtualStream,
|
||||
};
|
||||
@@ -150,21 +175,24 @@ export class ImageManager {
|
||||
|
||||
public async start() {
|
||||
// lets setup s3
|
||||
const s3Descriptor: plugins.tsclass.storage.IS3Descriptor =
|
||||
await this.cloudlyRef.config.appData.waitForAndGetKey('s3Descriptor');
|
||||
const s3Descriptor =
|
||||
await this.cloudlyRef.config.appData.waitForAndGetKey('s3Descriptor') as plugins.tsclass.storage.IS3Descriptor;
|
||||
console.log(this.cloudlyRef.config.data.s3Descriptor);
|
||||
this.smartbucketInstance = new plugins.smartbucket.SmartBucket(
|
||||
this.cloudlyRef.config.data.s3Descriptor,
|
||||
this.cloudlyRef.config.data.s3Descriptor!,
|
||||
);
|
||||
const bucket = await this.smartbucketInstance.getBucketByName(s3Descriptor.bucketName);
|
||||
await bucket.fastPut({ path: 'images/00init', contents: 'init' });
|
||||
const bucketName = s3Descriptor.bucketName!;
|
||||
const bucket = await this.smartbucketInstance.bucketExists(bucketName)
|
||||
? await this.smartbucketInstance.getBucketByName(bucketName)
|
||||
: await this.smartbucketInstance.createBucket(bucketName);
|
||||
await bucket.fastPut({ path: 'images/00init', contents: 'init', overwrite: true });
|
||||
|
||||
this.imageDir = await bucket.getDirectoryFromPath({
|
||||
path: '/images',
|
||||
});
|
||||
|
||||
// lets setup dockerstore
|
||||
await plugins.smartfile.fs.ensureDir(paths.dockerImageStoreDir);
|
||||
await plugins.fsPromises.mkdir(paths.dockerImageStoreDir, { recursive: true });
|
||||
this.dockerImageStore = new plugins.docker.DockerImageStore({
|
||||
localDirPath: paths.dockerImageStoreDir,
|
||||
bucketDir: this.imageDir,
|
||||
|
||||
@@ -34,10 +34,10 @@ export class ClusterNode extends plugins.smartdata.SmartDataDbDoc<
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.servezoneInterfaces.data.IClusterNode['data'];
|
||||
public data!: plugins.servezoneInterfaces.data.IClusterNode['data'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { logger } from '../logger.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { CloudlyNodeManager } from './classes.nodemanager.js';
|
||||
import type { Cluster } from '../manager.cluster/classes.cluster.js';
|
||||
|
||||
export class CurlFresh {
|
||||
public optionsArg = {
|
||||
@@ -39,33 +40,33 @@ bash -c "npm config set registry ${this.optionsArg.npmRegistry}"
|
||||
bash -c "pnpm install -g @serve.zone/spark"
|
||||
|
||||
# lets install the spark daemon
|
||||
bash -c "spark installdaemon"
|
||||
|
||||
# TODO: start spark with jump code
|
||||
bash -c "spark installdaemon --mode=coreflow-node --cloudlyUrl='__CLOUDLY_URL__' --jumpcode='__JUMPCODE__'"
|
||||
`,
|
||||
};
|
||||
|
||||
public nodeManagerRef: CloudlyNodeManager;
|
||||
public curlFreshRoute: plugins.typedserver.servertools.Route;
|
||||
public handler = new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
|
||||
public async handleRequest(ctx: plugins.typedserver.IRequestContext): Promise<Response> {
|
||||
logger.log('info', 'curlfresh handler called. a server might be coming online soon :)');
|
||||
const scriptname = req.params.scriptname;
|
||||
const scriptname = ctx.params.scriptname;
|
||||
switch (scriptname) {
|
||||
case 'setup.sh':
|
||||
logger.log('info', 'sending setup.sh');
|
||||
res.type('application/x-sh');
|
||||
res.send(this.scripts['setup.sh']);
|
||||
break;
|
||||
return new Response(this.scripts['setup.sh']
|
||||
.replaceAll('__CLOUDLY_URL__', ctx.url.searchParams.get('cloudlyUrl') || '')
|
||||
.replaceAll('__JUMPCODE__', ctx.url.searchParams.get('jumpcode') || ''), {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-sh',
|
||||
},
|
||||
});
|
||||
default:
|
||||
res.send('no script found');
|
||||
break;
|
||||
return new Response('no script found', { status: 404 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
constructor(nodeManagerRefArg: CloudlyNodeManager) {
|
||||
this.nodeManagerRef = nodeManagerRefArg;
|
||||
}
|
||||
public async getServerUserData(): Promise<string> {
|
||||
public async getServerUserData(clusterArg?: Cluster): Promise<string> {
|
||||
const sslMode =
|
||||
await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('sslMode');
|
||||
let protocol: 'http' | 'https';
|
||||
@@ -80,9 +81,19 @@ bash -c "spark installdaemon"
|
||||
const port =
|
||||
await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicPort');
|
||||
|
||||
let cloudlyUrl = `${protocol}://${domain}:${port}/`;
|
||||
let jumpcode = '';
|
||||
if (clusterArg?.data.userId) {
|
||||
const clusterUser = await this.nodeManagerRef.cloudlyRef.authManager.CUser.getInstance({
|
||||
id: clusterArg.data.userId,
|
||||
});
|
||||
jumpcode = clusterUser?.data.tokens?.[0]?.token || '';
|
||||
cloudlyUrl = clusterArg.data.cloudlyUrl || cloudlyUrl;
|
||||
}
|
||||
|
||||
const serverUserData = `#cloud-config
|
||||
runcmd:
|
||||
- curl -o- ${protocol}://${domain}:${port}/curlfresh/setup.sh | sh
|
||||
- curl -o- '${protocol}://${domain}:${port}/curlfresh/setup.sh?cloudlyUrl=${encodeURIComponent(cloudlyUrl)}&jumpcode=${encodeURIComponent(jumpcode)}' | sh
|
||||
`;
|
||||
console.log(serverUserData);
|
||||
return serverUserData;
|
||||
|
||||
@@ -9,7 +9,7 @@ export class CloudlyNodeManager {
|
||||
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||
public curlfreshInstance = new CurlFresh(this);
|
||||
|
||||
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
|
||||
public hetznerAccount?: plugins.hetznercloud.HetznerAccount;
|
||||
|
||||
public get db() {
|
||||
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||
@@ -65,13 +65,17 @@ export class CloudlyNodeManager {
|
||||
console.log(`Skipping node provisioning for cluster ${cluster.id} - setupMode is ${cluster.data.setupMode || 'manual'}`);
|
||||
continue;
|
||||
}
|
||||
const hetznerAccount = this.hetznerAccount;
|
||||
if (!hetznerAccount) {
|
||||
throw new Error('Hetzner account is not configured');
|
||||
}
|
||||
|
||||
// get existing nodes
|
||||
const nodes = await this.getNodesByCluster(cluster);
|
||||
|
||||
// if there is no node, create one
|
||||
if (nodes.length === 0) {
|
||||
const hetznerServer = await this.hetznerAccount.createServer({
|
||||
const hetznerServer = await hetznerAccount.createServer({
|
||||
name: plugins.smartunique.uniSimple('node'),
|
||||
location: 'nbg1',
|
||||
type: 'cpx41',
|
||||
@@ -79,7 +83,7 @@ export class CloudlyNodeManager {
|
||||
clusterId: cluster.id,
|
||||
priority: '1',
|
||||
},
|
||||
userData: await this.curlfreshInstance.getServerUserData(),
|
||||
userData: await this.curlfreshInstance.getServerUserData(cluster),
|
||||
});
|
||||
|
||||
// First create BareMetal record
|
||||
@@ -94,12 +98,12 @@ export class CloudlyNodeManager {
|
||||
);
|
||||
// if there is a node, make sure that it exists
|
||||
for (const node of nodes) {
|
||||
const hetznerServers = await this.hetznerAccount.getServersByLabel({
|
||||
const hetznerServers = await hetznerAccount.getServersByLabel({
|
||||
clusterId: cluster.id,
|
||||
});
|
||||
if (!hetznerServers || hetznerServers.length === 0) {
|
||||
console.log(`node ${node.id} does not exist in the real world. Creating it now...`);
|
||||
const hetznerServer = await this.hetznerAccount.createServer({
|
||||
const hetznerServer = await hetznerAccount.createServer({
|
||||
name: plugins.smartunique.uniSimple('node'),
|
||||
location: 'nbg1',
|
||||
type: 'cpx41',
|
||||
@@ -107,6 +111,7 @@ export class CloudlyNodeManager {
|
||||
clusterId: cluster.id,
|
||||
priority: '1',
|
||||
},
|
||||
userData: await this.curlfreshInstance.getServerUserData(cluster),
|
||||
});
|
||||
|
||||
// First create BareMetal record
|
||||
|
||||
@@ -10,11 +10,11 @@ export class PlatformBinding extends plugins.smartdata.SmartDataDbDoc<
|
||||
public static async upsertBinding(
|
||||
bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding,
|
||||
) {
|
||||
const existingBinding =
|
||||
bindingArg.id &&
|
||||
(await this.getInstance({
|
||||
const existingBinding = bindingArg.id
|
||||
? await this.getInstance({
|
||||
id: bindingArg.id,
|
||||
}));
|
||||
})
|
||||
: undefined;
|
||||
const binding = existingBinding || new PlatformBinding();
|
||||
const timestamp = Date.now();
|
||||
|
||||
|
||||
@@ -66,37 +66,32 @@ export class CloudlyRegistryManager {
|
||||
}
|
||||
|
||||
public async handleHttpRequest(
|
||||
req: plugins.typedserver.Request,
|
||||
res: plugins.typedserver.Response,
|
||||
) {
|
||||
ctx: plugins.typedserver.IRequestContext,
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const requestUrl = new URL((req as any).originalUrl || req.url || '/', 'http://localhost');
|
||||
const requestUrl = ctx.url;
|
||||
|
||||
if (requestUrl.pathname === '/v2/token') {
|
||||
await this.handleTokenRequest(req, res, requestUrl);
|
||||
return;
|
||||
return await this.handleTokenRequest(ctx, requestUrl);
|
||||
}
|
||||
|
||||
if (!this.started) {
|
||||
res.status(503);
|
||||
res.end('registry is not ready');
|
||||
return;
|
||||
return new Response('registry is not ready', { status: 503 });
|
||||
}
|
||||
|
||||
const rawBody = await this.getRawBody(req);
|
||||
const rawBody = Buffer.from(await ctx.request.arrayBuffer());
|
||||
const response = await this.smartRegistry.handleRequest({
|
||||
method: req.method || 'GET',
|
||||
method: ctx.method || 'GET',
|
||||
path: requestUrl.pathname,
|
||||
query: Object.fromEntries(requestUrl.searchParams),
|
||||
headers: this.headersToRecord(req.headers),
|
||||
headers: this.headersToRecord(ctx.headers),
|
||||
rawBody: rawBody.length > 0 ? rawBody : undefined,
|
||||
});
|
||||
|
||||
await this.sendRegistryResponse(res, response);
|
||||
return this.createRegistryResponse(response);
|
||||
} catch (error) {
|
||||
logger.log('error', `registry request failed: ${(error as Error).message}`);
|
||||
res.status(500);
|
||||
res.end('registry request failed');
|
||||
return new Response('registry request failed', { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,16 +254,17 @@ export class CloudlyRegistryManager {
|
||||
}
|
||||
|
||||
private async handleTokenRequest(
|
||||
req: plugins.typedserver.Request,
|
||||
res: plugins.typedserver.Response,
|
||||
ctx: plugins.typedserver.IRequestContext,
|
||||
requestUrl: URL,
|
||||
) {
|
||||
const user = await this.authenticateRequest(req);
|
||||
): Promise<Response> {
|
||||
const user = await this.authenticateRequest(ctx);
|
||||
if (!user) {
|
||||
res.status(401);
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Cloudly Registry"');
|
||||
res.end('authentication required');
|
||||
return;
|
||||
return new Response('authentication required', {
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': 'Basic realm="Cloudly Registry"',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const requestedScopes = this.getRequestedOciScopes(requestUrl.searchParams);
|
||||
@@ -277,9 +273,7 @@ export class CloudlyRegistryManager {
|
||||
return action === 'push' || action === 'delete';
|
||||
});
|
||||
if (requestedWriteAccess && !user.canWrite) {
|
||||
res.status(403);
|
||||
res.end('registry write access denied');
|
||||
return;
|
||||
return new Response('registry write access denied', { status: 403 });
|
||||
}
|
||||
|
||||
const token = await this.smartRegistry.getAuthManager().createOciToken(
|
||||
@@ -287,22 +281,26 @@ export class CloudlyRegistryManager {
|
||||
requestedScopes,
|
||||
3600,
|
||||
);
|
||||
res.status(200);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
token,
|
||||
access_token: token,
|
||||
expires_in: 3600,
|
||||
issued_at: new Date().toISOString(),
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async authenticateRequest(
|
||||
req: plugins.typedserver.Request,
|
||||
ctx: plugins.typedserver.IRequestContext,
|
||||
): Promise<TAuthenticatedRegistryUser | null> {
|
||||
const credentials = this.getBasicCredentials(req);
|
||||
const credentials = this.getBasicCredentials(ctx);
|
||||
if (!credentials) {
|
||||
return null;
|
||||
}
|
||||
@@ -332,8 +330,8 @@ export class CloudlyRegistryManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
private getBasicCredentials(req: plugins.typedserver.Request) {
|
||||
const authHeader = req.headers.authorization;
|
||||
private getBasicCredentials(ctx: plugins.typedserver.IRequestContext) {
|
||||
const authHeader = ctx.headers.get('authorization');
|
||||
if (!authHeader?.startsWith('Basic ')) {
|
||||
return null;
|
||||
}
|
||||
@@ -370,48 +368,49 @@ export class CloudlyRegistryManager {
|
||||
return `${this.cloudlyRef.config.data.sslMode === 'none' ? 'http' : 'https'}://${this.getRegistryHost()}`;
|
||||
}
|
||||
|
||||
private headersToRecord(headersArg: plugins.typedserver.Request['headers']) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(headersArg).map(([key, value]) => [
|
||||
key.toLowerCase(),
|
||||
Array.isArray(value) ? value.join(', ') : value || '',
|
||||
]),
|
||||
);
|
||||
private headersToRecord(headersArg: Headers) {
|
||||
const headers: Record<string, string> = {};
|
||||
headersArg.forEach((value, key) => {
|
||||
headers[key.toLowerCase()] = value;
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
|
||||
private async getRawBody(req: plugins.typedserver.Request) {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of req as any) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
private async sendRegistryResponse(
|
||||
res: plugins.typedserver.Response,
|
||||
private createRegistryResponse(
|
||||
responseArg: plugins.smartregistry.IResponse,
|
||||
) {
|
||||
res.status(responseArg.status);
|
||||
): Response {
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of Object.entries(responseArg.headers)) {
|
||||
res.setHeader(key, value);
|
||||
headers.set(key, value);
|
||||
}
|
||||
|
||||
if (!responseArg.body) {
|
||||
res.end();
|
||||
return;
|
||||
return new Response(null, {
|
||||
status: responseArg.status,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (responseArg.body instanceof ReadableStream) {
|
||||
plugins.stream.Readable.fromWeb(responseArg.body as any).pipe(res);
|
||||
return;
|
||||
return new Response(responseArg.body, {
|
||||
status: responseArg.status,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(responseArg.body) || typeof responseArg.body === 'string') {
|
||||
res.end(responseArg.body);
|
||||
return;
|
||||
return new Response(responseArg.body as BodyInit, {
|
||||
status: responseArg.status,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', responseArg.headers['Content-Type'] || 'application/json');
|
||||
res.end(JSON.stringify(responseArg.body));
|
||||
if (!headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
return new Response(JSON.stringify(responseArg.body), {
|
||||
status: responseArg.status,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ export class SecretBundle extends plugins.smartdata.SmartDataDbDoc<
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.servezoneInterfaces.data.ISecretBundle['data'];
|
||||
public data!: plugins.servezoneInterfaces.data.ISecretBundle['data'];
|
||||
|
||||
public async getSecretGroups() {
|
||||
const secretGroups: SecretGroup[] = [];
|
||||
|
||||
@@ -14,8 +14,8 @@ export class SecretGroup extends plugins.smartdata.SmartDataDbDoc<
|
||||
* the insatnce id. This should be a random id, except for default
|
||||
*/
|
||||
@plugins.smartdata.unI()
|
||||
id: string;
|
||||
id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
data: plugins.servezoneInterfaces.data.ISecretGroup['data'];
|
||||
data!: plugins.servezoneInterfaces.data.ISecretGroup['data'];
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ export class CloudlySecretManager {
|
||||
|
||||
// INSTANCE
|
||||
public cloudlyRef: Cloudly;
|
||||
public projectinfo = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
|
||||
public projectinfo = plugins.projectinfo.ProjectinfoNpm.create(paths.packageDir);
|
||||
public serviceQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir);
|
||||
public typedrouter: plugins.typedrequest.TypedRouter;
|
||||
public typedrouter!: plugins.typedrequest.TypedRouter;
|
||||
|
||||
get db() {
|
||||
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||
@@ -40,7 +40,7 @@ export class CloudlySecretManager {
|
||||
new plugins.typedrequest.TypedHandler(
|
||||
'getSecretBundles',
|
||||
async (dataArg, toolsArg) => {
|
||||
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||
dataArg.identity.jwt;
|
||||
const secretBundles = await SecretBundle.getInstances({});
|
||||
return {
|
||||
@@ -56,7 +56,7 @@ export class CloudlySecretManager {
|
||||
|
||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretbundle.IReq_GetSecretBundleById>(
|
||||
new plugins.typedrequest.TypedHandler('getSecretBundleById', async (dataArg, toolsArg) => {
|
||||
await toolsArg.passGuards([this.cloudlyRef.authManager.adminOrClusterIdentityGuard], dataArg);
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminOrClusterIdentityGuard], dataArg);
|
||||
const secretBundle = await SecretBundle.getInstance({
|
||||
id: dataArg.secretBundleId,
|
||||
});
|
||||
@@ -108,7 +108,7 @@ export class CloudlySecretManager {
|
||||
new plugins.typedrequest.TypedHandler(
|
||||
'getSecretGroups',
|
||||
async (dataArg, toolsArg) => {
|
||||
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||
dataArg.identity.jwt;
|
||||
const secretGroups = await SecretGroup.getInstances({});
|
||||
return {
|
||||
@@ -176,6 +176,9 @@ export class CloudlySecretManager {
|
||||
const authorization = await wantedBundle.getAuthorizationFromAuthKey(
|
||||
dataArg.secretBundleAuthorization.secretAccessKey,
|
||||
);
|
||||
if (!authorization) {
|
||||
throw new plugins.typedrequest.TypedResponseError('secret bundle authorization not found');
|
||||
}
|
||||
return {
|
||||
flatKeyValueObject: await wantedBundle.getKeyValueObjectForEnvironment(
|
||||
authorization.environment,
|
||||
|
||||
@@ -37,10 +37,10 @@ export class Service extends plugins.smartdata.SmartDataDbDoc<
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.servezoneInterfaces.data.IService['data'];
|
||||
public data!: plugins.servezoneInterfaces.data.IService['data'];
|
||||
|
||||
/**
|
||||
* a service runs in a specific environment
|
||||
|
||||
@@ -94,6 +94,9 @@ export class ServiceManager {
|
||||
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,
|
||||
@@ -112,6 +115,9 @@ export class ServiceManager {
|
||||
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,
|
||||
});
|
||||
@@ -136,6 +142,9 @@ export class ServiceManager {
|
||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(
|
||||
'deleteServiceById',
|
||||
async (dataArg) => {
|
||||
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
||||
this.cloudlyRef.authManager.adminIdentityGuard,
|
||||
]);
|
||||
const service = await Service.getInstance({
|
||||
id: dataArg.serviceId,
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as servezoneInterfaces from '@serve.zone/interfaces';
|
||||
export class CloudlySettingsManager {
|
||||
public cloudlyRef: Cloudly;
|
||||
public readyDeferred = plugins.smartpromise.defer();
|
||||
public settingsStore: plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
|
||||
public settingsStore!: plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
|
||||
|
||||
constructor(cloudlyRefArg: Cloudly) {
|
||||
this.cloudlyRef = cloudlyRefArg;
|
||||
@@ -196,7 +196,7 @@ export class CloudlySettingsManager {
|
||||
return { success: false, message: `Unknown provider: ${provider}` };
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, message: `Connection test failed: ${error.message}` };
|
||||
return { success: false, message: `Connection test failed: ${error instanceof Error ? error.message : String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,9 +232,10 @@ export class CloudlySettingsManager {
|
||||
message: 'Settings updated successfully'
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to update settings: ${error.message}`
|
||||
message: `Failed to update settings: ${errorMessage}`
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -254,9 +255,10 @@ export class CloudlySettingsManager {
|
||||
message: `Setting ${requestData.key} cleared successfully`
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to clear setting: ${error.message}`
|
||||
message: `Failed to clear setting: ${errorMessage}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +64,10 @@ export class TaskExecution extends plugins.smartdata.SmartDataDbDoc<
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.servezoneInterfaces.data.ITaskExecution['data'];
|
||||
public data!: plugins.servezoneInterfaces.data.ITaskExecution['data'];
|
||||
|
||||
/**
|
||||
* Add a log entry to the execution
|
||||
@@ -162,4 +162,4 @@ export class TaskExecution extends plugins.smartdata.SmartDataDbDoc<
|
||||
|
||||
return oldExecutions.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export class CloudlyTaskManager {
|
||||
taskName: string,
|
||||
triggeredBy: 'schedule' | 'manual' | 'system',
|
||||
userId?: string
|
||||
): Promise<TaskExecution> {
|
||||
): Promise<TaskExecution | null> {
|
||||
const task = this.taskRegistry.get(taskName);
|
||||
const info = this.taskInfo.get(taskName);
|
||||
|
||||
@@ -298,6 +298,9 @@ export class CloudlyTaskManager {
|
||||
'manual',
|
||||
reqArg.userId
|
||||
);
|
||||
if (!execution) {
|
||||
throw new Error(`Task ${reqArg.taskName} did not start`);
|
||||
}
|
||||
|
||||
return {
|
||||
execution: await execution.createSavableObject(),
|
||||
@@ -336,6 +339,7 @@ export class CloudlyTaskManager {
|
||||
if (deletedCount > 0) {
|
||||
logger.log('info', `Cleaned up ${deletedCount} old task executions`);
|
||||
}
|
||||
await this.taskBufferManager.start();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,8 @@ import * as plugins from '../plugins.js';
|
||||
import { CloudlyTaskManager } from './classes.taskmanager.js';
|
||||
import { logger } from '../logger.js';
|
||||
|
||||
const getErrorMessage = (errorArg: unknown) => errorArg instanceof Error ? errorArg.message : String(errorArg);
|
||||
|
||||
/**
|
||||
* Create and register all predefined tasks
|
||||
*/
|
||||
@@ -74,7 +76,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
await execution?.addLog(`Cloudflare sync done: ${created} created, ${updated} updated`, 'success');
|
||||
return { created, updated, totalZones: zones.length };
|
||||
} catch (error) {
|
||||
await execution?.addLog(`Cloudflare sync error: ${error.message}`, 'error');
|
||||
await execution?.addLog(`Cloudflare sync error: ${getErrorMessage(error)}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -122,7 +124,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
await execution?.addLog(`Syncing DNS entry: ${entry.data.name}.${entry.data.zone}`, 'info');
|
||||
syncedCount++;
|
||||
} catch (error) {
|
||||
await execution?.addLog(`Failed to sync ${entry.data.name}: ${error.message}`, 'warning');
|
||||
await execution?.addLog(`Failed to sync ${entry.data.name}: ${getErrorMessage(error)}`, 'warning');
|
||||
failedCount++;
|
||||
}
|
||||
}
|
||||
@@ -133,7 +135,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
|
||||
return { synced: syncedCount, failed: failedCount };
|
||||
} catch (error) {
|
||||
await execution?.addLog(`DNS sync error: ${error.message}`, 'error');
|
||||
await execution?.addLog(`DNS sync error: ${getErrorMessage(error)}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -194,7 +196,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
|
||||
return { renewed: renewedCount, upToDate: upToDateCount };
|
||||
} catch (error) {
|
||||
await execution?.addLog(`Certificate renewal error: ${error.message}`, 'error');
|
||||
await execution?.addLog(`Certificate renewal error: ${getErrorMessage(error)}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -247,7 +249,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
images: deletedImages,
|
||||
};
|
||||
} catch (error) {
|
||||
await execution?.addLog(`Cleanup error: ${error.message}`, 'error');
|
||||
await execution?.addLog(`Cleanup error: ${getErrorMessage(error)}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -279,7 +281,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
|
||||
let healthyCount = 0;
|
||||
let unhealthyCount = 0;
|
||||
const issues = [];
|
||||
const issues: Array<{ deploymentId: string; serviceId: string; issue: string }> = [];
|
||||
|
||||
for (const deployment of deployments) {
|
||||
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||
@@ -316,7 +318,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
|
||||
return { healthy: healthyCount, unhealthy: unhealthyCount, issues };
|
||||
} catch (error) {
|
||||
await execution?.addLog(`Health check error: ${error.message}`, 'error');
|
||||
await execution?.addLog(`Health check error: ${getErrorMessage(error)}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -345,7 +347,13 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
// Get all nodes
|
||||
const nodes = await taskManager.cloudlyRef.nodeManager.CClusterNode.getInstances({});
|
||||
|
||||
const report = {
|
||||
const report: {
|
||||
timestamp: number;
|
||||
nodes: Array<{ nodeId: string; nodeName: string; cpu: number; memory: number; disk: number }>;
|
||||
totalCpu: number;
|
||||
totalMemory: number;
|
||||
totalDisk: number;
|
||||
} = {
|
||||
timestamp: Date.now(),
|
||||
nodes: [],
|
||||
totalCpu: 0,
|
||||
@@ -388,7 +396,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
|
||||
return report;
|
||||
} catch (error) {
|
||||
await execution?.addLog(`Resource report error: ${error.message}`, 'error');
|
||||
await execution?.addLog(`Resource report error: ${getErrorMessage(error)}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -428,7 +436,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
await execution?.addLog(`Database maintenance error: ${error.message}`, 'error');
|
||||
await execution?.addLog(`Database maintenance error: ${getErrorMessage(error)}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -454,7 +462,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vulnerabilities = [];
|
||||
const vulnerabilities: Array<{ type: string; severity: string; image: string; version: string }> = [];
|
||||
|
||||
// Check for exposed ports
|
||||
await execution?.addLog('Checking for exposed ports...', 'info');
|
||||
@@ -497,7 +505,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
|
||||
return { vulnerabilities };
|
||||
} catch (error) {
|
||||
await execution?.addLog(`Security scan error: ${error.message}`, 'error');
|
||||
await execution?.addLog(`Security scan error: ${getErrorMessage(error)}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -549,7 +557,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||
networks: removedNetworks,
|
||||
};
|
||||
} catch (error) {
|
||||
await execution?.addLog(`Docker cleanup error: ${error.message}`, 'error');
|
||||
await execution?.addLog(`Docker cleanup error: ${getErrorMessage(error)}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
+4
-3
@@ -2,8 +2,9 @@
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'node:crypto';
|
||||
import * as stream from 'stream';
|
||||
import * as fsPromises from 'node:fs/promises';
|
||||
|
||||
export { path, crypto, stream };
|
||||
export { path, crypto, stream, fsPromises };
|
||||
|
||||
// @apiglobal scope
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
@@ -25,7 +26,7 @@ import * as tsclass from '@tsclass/tsclass';
|
||||
export { tsclass };
|
||||
|
||||
// @push.rocks scope
|
||||
import * as npmextra from '@push.rocks/npmextra';
|
||||
import * as smartconfig from '@push.rocks/smartconfig';
|
||||
import * as projectinfo from '@push.rocks/projectinfo';
|
||||
import * as qenv from '@push.rocks/qenv';
|
||||
import * as smartacme from '@push.rocks/smartacme';
|
||||
@@ -54,7 +55,7 @@ import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||
import * as typedserver from '@api.global/typedserver';
|
||||
|
||||
export {
|
||||
npmextra,
|
||||
smartconfig,
|
||||
projectinfo,
|
||||
qenv,
|
||||
smartacme,
|
||||
|
||||
Reference in New Issue
Block a user