clever-tools
Version:
Command Line Interface for Clever Cloud.
278 lines (239 loc) • 10.4 kB
JavaScript
import {
createNetworkGroup,
deleteNetworkGroup,
getNetworkGroup,
getNetworkGroupWireGuardConfiguration,
listNetworkGroups,
} from '@clevercloud/client/esm/api/v4/network-group.js';
import crypto from 'node:crypto';
import { searchNetworkGroupOrResource } from '../clever-client/ng.js';
import { styleText } from '../lib/style-text.js';
import { Logger } from '../logger.js';
import { checkMembersToLink } from './ng-resources.js';
import * as Organisation from './organisation.js';
import { sendToApi } from './send-to-api.js';
import * as User from './user.js';
export const POLLING_TIMEOUT_MS = 30_000;
export const POLLING_INTERVAL_MS = 1000;
export const DOMAIN = 'cc-ng.cloud';
const TYPE_PREFIXES = {
app_: 'APPLICATION',
addon_: 'ADDON',
external_: 'EXTERNAL',
};
/**
* Ask for a Network Group creation
* @param {string} label The Network Group label
* @param {string} description The Network Group description
* @param {string} tags The Network Group tags
* @param {Array<string>} membersIds The members to link to the Network Group
* @param {string} orgaIdOrName The owner ID or name
* @throws {Error} If the Network Group label is missing
*/
export async function create(label, description, tags, membersIds, orgaIdOrName) {
const id = `ng_${crypto.randomUUID()}`;
const ownerId = await getOwnerIdFromOrgaIdOrName(orgaIdOrName);
if (membersIds?.length > 0) {
await checkMembersToLink(membersIds, ownerId);
}
const members = constructMembers(id, membersIds || []);
const body = { ownerId, id, label, description, tags, members };
Logger.info(`Creating Network Group ${label} (${id}) from owner ${ownerId}`);
Logger.info(`${members.length} members will be added: ${members.map((m) => m.id).join(', ')}`);
Logger.debug(`Sending body: ${JSON.stringify(body, null, 2)}`);
await createNetworkGroup({ ownerId }, body).then(sendToApi);
await pollNetworkGroup(ownerId, id, { waitForMembers: membersIds });
Logger.info(`Network Group ${label} (${id}) created from owner ${ownerId}`);
}
/**
* Ask for a Network Group deletion
* @param {object} ngIdOrLabel The Network Group ID or Label
* @param {object} orgaIdOrName The owner ID or name
* @throws {Error} If the Network Group is not found
*/
export async function destroy(ngIdOrLabel, orgaIdOrName) {
const [found] = await searchNgOrResource(ngIdOrLabel, orgaIdOrName, 'NetworkGroup');
if (!found) {
throw new Error(`Network Group ${styleText('red', ngIdOrLabel.ngId ?? ngIdOrLabel.ngResourceLabel)} not found`);
}
await deleteNetworkGroup({ ownerId: found.ownerId, networkGroupId: found.id }).then(sendToApi);
Logger.info(`Deleting Network Group ${found.id} from owner ${found.ownerId}`);
await pollNetworkGroup(found.ownerId, found.id, { waitForDeletion: true });
Logger.info(`Network Group ${found.id} deleted from owner ${found.ownerId}`);
}
/**
* Get the Wireguard configuration of a Network Group peer
* @param {object} peerIdOrLabel The Peer ID or Label
* @param {object} ngIdOrLabel The Network Group ID or Label
* @param {object} orgaIdOrName The owner ID or name
* @returns {Promise<Object>} The Peer Wireguard configuration
* @throws {Error} If the Peer is not found
* @throws {Error} If the Network Group is not found
* @throws {Error} If the Peer is not in the Network Group
*/
export async function getPeerConfig(peerIdOrLabel, ngIdOrLabel, orgaIdOrName) {
const [parentNg] = await searchNgOrResource(ngIdOrLabel, orgaIdOrName, 'NetworkGroup');
if (!parentNg) {
throw new Error(`Network Group ${styleText('red', ngIdOrLabel.ngId ?? ngIdOrLabel.ngResourceLabel)} not found`);
}
const [peer] = await searchNgOrResource(peerIdOrLabel, orgaIdOrName, 'Peer');
// peer.id is catched as a ngResourceLabel as it's a string with no distinctive prefix for now, it will change from API
if (
!peer ||
(peerIdOrLabel.ngResourceLabel &&
peerIdOrLabel.ngResourceLabel !== peer.label &&
peerIdOrLabel.ngResourceLabel !== peer.id)
) {
throw new Error(`Peer ${styleText('red', peerIdOrLabel.ngResourceLabel ?? peerIdOrLabel.member)} not found`);
}
if (!parentNg.peers.find((p) => p.id === peer.id)) {
throw new Error(`Peer ${styleText('red', peer.id)} is not in Network Group ${styleText('red', parentNg.id)}`);
}
Logger.debug(`Getting configuration for Peer ${peer.id}`);
const result = await getNetworkGroupWireGuardConfiguration({
ownerId: parentNg.ownerId,
networkGroupId: parentNg.id,
peerId: peer.id,
}).then(sendToApi);
Logger.debug(`Received from API:\n${JSON.stringify(result, null, 2)}`);
return result;
}
/**
* Get a Network group from an owner with members and peers
* @param {string} networkGroupId The Network Group ID
* @param {string} orgaIdOrName The owner ID or name
* @returns {Promise<Array<Object>>} The Network Groups
*/
export async function getNG(networkGroupId, orgaIdOrName) {
const ownerId = await getOwnerIdFromOrgaIdOrName(orgaIdOrName);
Logger.info(`Get Network Group ${networkGroupId} for owner ${ownerId}`);
const result = await getNetworkGroup({ networkGroupId, ownerId }).then(sendToApi);
Logger.debug(`Received from API:\n${JSON.stringify(result, null, 2)}`);
return result;
}
/**
* Get all Network Groups from an owner with members and peers
* @param {string} orgaIdOrName The owner ID or name
* @returns {Promise<Array<Object>>} The Network Groups
*/
export async function getAllNGs(orgaIdOrName) {
const ownerId = await getOwnerIdFromOrgaIdOrName(orgaIdOrName);
Logger.info(`Listing Network Groups from owner ${ownerId}`);
const result = await listNetworkGroups({ ownerId }).then(sendToApi);
Logger.debug(`Received from API:\n${JSON.stringify(result, null, 2)}`);
return result;
}
/**
* Search a Network Group or a resource (member/peer)
* @param {string|Object} idOrLabel The ID or label to look for
* @param {Object} orgaIdOrName The owner ID or name
* @param {string} [type] Look only for a specific type (NetworkGroup, Member, CleverPeer, ExternalPeer, Peer), can be 'single', default to 'all'
* @param {boolean} exactMatch Look for exact match, default to true
* @throws {Error} If multiple Network Groups or member/peer are found in single_result mode
* @returns {Promise<Object>} Found results
*/
export async function searchNgOrResource(idOrLabel, orgaIdOrName, type = 'all', exactMatch = true) {
const ownerId = await getOwnerIdFromOrgaIdOrName(orgaIdOrName);
// If idOrLabel is a string we use it, or we look through multiple keys
const query =
typeof idOrLabel === 'string' ? idOrLabel : (idOrLabel.ngId ?? idOrLabel.memberId ?? idOrLabel.ngResourceLabel);
const found = await searchNetworkGroupOrResource({ ownerId, query }).then(sendToApi);
let filtered = found;
switch (type) {
case 'all':
case 'single':
break;
case 'Peer':
filtered = found.filter((f) => f.type === 'CleverPeer' || f.type === 'ExternalPeer');
break;
case 'CleverPeer':
case 'ExternalPeer':
case 'Member':
case 'NetworkGroup':
filtered = found.filter((f) => f.type === type);
break;
default:
throw new Error(`Unsupported type: ${type}`);
}
if (exactMatch) {
filtered = filtered.filter((f) => f.id === query || f.label === query);
}
if (filtered.length > 1 && type !== 'all') {
throw new Error(`Multiple resources found for ${styleText('red', query)}, use ID instead:
${filtered.map((f) => ` • ${f.id} ${styleText('grey', `(${f.label} - ${f.type})`)}`).join('\n')}`);
}
// Deduplicate results
return filtered.filter((item, index, array) => array.findIndex((element) => element.id === item.id) === index);
}
/**
* Construct members from members_ids
* @param {string} ngId The Network Group ID
* @param {Array<string>} membersIds The members IDs
* @returns {Array<Object>} Array of members with id, domainName and kind
*/
export function constructMembers(ngId, membersIds) {
return membersIds.map((id) => {
const domainName = `${id}.m.${ngId}.${DOMAIN}`;
const prefixToType = TYPE_PREFIXES;
return {
id,
domainName,
// Get kind from prefix match in id (app_*, addon_*, external_*) or default to 'APPLICATION'
kind: prefixToType[Object.keys(prefixToType).find((p) => id.startsWith(p))] ?? TYPE_PREFIXES.app_,
};
});
}
/**
* Poll Network Groups to check its status and members
* @param {string} ownerId The owner ID
* @param {string} ngId The Network Group ID
* @param {Array<string>} waitForMembers The members IDs to wait for
* @param {boolean} waitForDeletion Wait for the Network Group deletion
* @throws {Error} When timeout is reached
* @returns {Promise<void>}
*/
async function pollNetworkGroup(ownerId, ngId, { waitForMembers = null, waitForDeletion = false } = {}) {
return new Promise((resolve, reject) => {
Logger.info(`Polling Network Groups from owner ${ownerId}`);
const timeoutTime = Date.now() + POLLING_TIMEOUT_MS;
async function pollOnce() {
if (Date.now() > timeoutTime) {
const action = waitForDeletion ? 'deletion of' : 'creation of';
reject(new Error(`Timeout while checking ${action} Network Group ${ngId}`));
return;
}
try {
const ngs = await listNetworkGroups({ ownerId }).then(sendToApi);
const ng = ngs.find((ng) => ng.id === ngId);
if (waitForDeletion && !ng) {
resolve();
return;
}
if (!waitForDeletion && ng) {
if (waitForMembers?.length) {
const members = ng.members.filter((member) => waitForMembers.includes(member.id));
if (members.length !== waitForMembers.length) {
Logger.debug(`Waiting for members: ${waitForMembers.join(', ')}`);
setTimeout(pollOnce, POLLING_INTERVAL_MS);
return;
}
}
resolve();
return;
}
setTimeout(pollOnce, POLLING_INTERVAL_MS);
} catch (error) {
reject(error);
}
}
pollOnce();
});
}
/**
* Get the owner ID from an Organisation ID or name
* @param {object} orgaIdOrName The Organisation ID or name
* @returns {Promise<string>} The owner ID
*/
async function getOwnerIdFromOrgaIdOrName(orgaIdOrName) {
return orgaIdOrName != null ? Organisation.getId(orgaIdOrName) : User.getCurrentId();
}