import * as plugins from '../plugins.js'; import { Cloudly } from '../classes.cloudly.js'; import { Cluster } from '../manager.cluster/classes.cluster.js'; import { ClusterNode } from './classes.clusternode.js'; import { CurlFresh } from './classes.curlfresh.js'; export class CloudlyNodeManager { public cloudlyRef: Cloudly; public typedRouter = new plugins.typedrequest.TypedRouter(); public curlfreshInstance = new CurlFresh(this); public hetznerAccount: plugins.hetznercloud.HetznerAccount; public get db() { return this.cloudlyRef.mongodbConnector.smartdataDb; } public CClusterNode = plugins.smartdata.setDefaultManagerForDoc(this, ClusterNode); constructor(cloudlyRefArg: Cloudly) { this.cloudlyRef = cloudlyRefArg; /** * is used be serverconfig module on the node to get the actual node config */ this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getNodeConfig', async (requestData) => { const nodeId = requestData.nodeId; const node = await this.CClusterNode.getInstance({ id: nodeId, }); return { configData: await node.createSavableObject(), }; }, ), ); } public async start() { const hetznerToken = await this.cloudlyRef.settingsManager.getSetting('hetznerToken'); if (!hetznerToken) { console.log('warn', 'No Hetzner token configured in settings. Hetzner features will be disabled.'); return; } this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(hetznerToken); } public async stop() {} /** * creates the node infrastructure on hetzner * ensures that there are exactly the resources that are needed * no more, no less */ public async ensureNodeInfrastructure() { // get all clusters const allClusters = await this.cloudlyRef.clusterManager.getAllClusters(); for (const cluster of allClusters) { // Skip clusters that are not set up for Hetzner auto-provisioning if (cluster.data.setupMode !== 'hetzner') { console.log(`Skipping node provisioning for cluster ${cluster.id} - setupMode is ${cluster.data.setupMode || 'manual'}`); continue; } // 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({ name: plugins.smartunique.uniSimple('node'), location: 'nbg1', type: 'cpx41', labels: { clusterId: cluster.id, priority: '1', }, userData: await this.curlfreshInstance.getServerUserData(), }); // First create BareMetal record const baremetal = await this.cloudlyRef.baremetalManager.createBaremetalFromHetznerServer(hetznerServer); const newNode = await ClusterNode.createFromHetznerServer(hetznerServer, cluster.id, baremetal.id); await baremetal.assignNode(newNode.id); console.log(`cluster created new node for cluster ${cluster.id}`); } else { console.log( `cluster ${cluster.id} already has nodes. Making sure that they actually exist in the real world...`, ); // if there is a node, make sure that it exists for (const node of nodes) { const hetznerServers = await this.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({ name: plugins.smartunique.uniSimple('node'), location: 'nbg1', type: 'cpx41', labels: { clusterId: cluster.id, priority: '1', }, }); // First create BareMetal record const baremetal = await this.cloudlyRef.baremetalManager.createBaremetalFromHetznerServer(hetznerServer); const newNode = await ClusterNode.createFromHetznerServer(hetznerServer, cluster.id, baremetal.id); await baremetal.assignNode(newNode.id); } } } } } public async getNodesByCluster(clusterArg: Cluster) { const results = await this.CClusterNode.getInstances({ data: { clusterId: clusterArg.id, }, }); return results; } }