clever-tools
Version:
Command Line Interface for Clever Cloud.
281 lines (230 loc) • 10.7 kB
JavaScript
import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
import * as networkGroupApi from '@clevercloud/client/esm/api/v4/network-group.js';
import crypto from 'node:crypto';
import { setTimeout } from 'node:timers/promises';
import { styleText } from '../lib/style-text.js';
import { Logger } from '../logger.js';
import * as networkGroup from './ng.js';
import { sendToApi } from './send-to-api.js';
/**
* Create an external peer and link its parent member to the Network Group
* @param {object} ngIdOrLabel The Network Group ID or Label
* @param {string} peerLabel External peer label
* @param {string} publicKey External peer public key
* @param {object} org Organisation ID or name
* @throws {Error} If a valid peer label is not provided
* @throws {Error} If the Network Group is not found
* @throws {Error} If the parent member is not linked to the Network Group
* @throws {Error} If the external peer is not linked to the Network Group
*/
export async function createExternalPeerWithParent(ngIdOrLabel, peerLabel, publicKey, org) {
if (!peerLabel) {
throw new Error('A valid peer label is required');
}
const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
if (!ng) {
throw new Error(`Network Group ${styleText('red', ngIdOrLabel.ngId || ngIdOrLabel.ngResourceLabel)} not found`);
}
// We define a parent member for the external peer
const id = `external_${crypto.randomUUID()}`;
const parentMember = {
id,
label: `Parent of ${peerLabel}`,
domainName: `${id}.m.${ng.id}.${networkGroup.DOMAIN}`,
kind: 'EXTERNAL',
};
Logger.info(`Creating a parent member ${parentMember.id} linked to Network Group ${ng.id}`);
await linkMember({ ngId: ng.id }, parentMember.id, org, parentMember.label);
const checkParentMember = await checkResource(ng.id, org, parentMember.id, true);
if (!checkParentMember) {
throw new Error(
`Parent member ${styleText('red', parentMember.id)} not linked to Network Group ${styleText('red', ng.id)}`,
);
}
Logger.info(`Parent member ${parentMember.id} created and linked to Network Group ${ng.id}`);
// We define the external peer, for now we only support client role
const body = {
peerRole: 'CLIENT',
publicKey,
label: peerLabel,
parentMember: parentMember.id,
};
Logger.info(`Adding external peer to Member ${parentMember.id} of Network Group ${ng.id}`);
Logger.debug('Sending body: ' + JSON.stringify(body, null, 2));
await networkGroupApi
.createNetworkGroupExternalPeer({ ownerId: ng.ownerId, networkGroupId: ng.id }, body)
.then(sendToApi);
const checkExternalPeer = await checkResource(ng.id, org, peerLabel, true, 'peer', 'label');
if (!checkExternalPeer) {
throw new Error(
`External peer ${styleText('red', peerLabel)} not linked to Network Group ${styleText('red', ng.id)}`,
);
}
Logger.info(`External peer ${peerLabel} added to Member ${parentMember.id} of Network Group ${ng.id}`);
}
/**
* Delete an external peer and its parent member from a Network Group
* @param {object} ngIdOrLabel Network Group ID or label
* @param {string} peerIdOrLabel External peer ID or label
* @param {object} org Organisation ID or name
* @throws {Error} If the Network Group is not found
* @throws {Error} If the External Peer is not found
* @throws {Error} If the External Peer is still linked to the Network Group
* @throws {Error} If the Parent Member is still linked to the Network Group
*/
export async function deleteExternalPeerWithParent(ngIdOrLabel, peerIdOrLabel, org) {
const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
if (!ng) {
throw new Error(`Network Group ${styleText('red', ngIdOrLabel.ngId || ngIdOrLabel.ngResourceLabel)} not found`);
}
const externalPeer = ng.peers.find((p) => {
return p.id === peerIdOrLabel || p.label === peerIdOrLabel;
});
if (!externalPeer) {
throw new Error(`External peer ${styleText('red', peerIdOrLabel)} not found`);
}
Logger.info(`Deleting external peer ${externalPeer.id} from Network Group ${ng.id}`);
await networkGroupApi
.deleteNetworkGroupExternalPeer({ ownerId: ng.ownerId, networkGroupId: ng.id, peerId: externalPeer.id })
.then(sendToApi);
const checkPeer = await checkResource(ng.id, org, externalPeer.id, false, 'peer');
if (!checkPeer) {
throw new Error(
`External peer ${styleText('red', externalPeer.id)} still linked to Network Group ${styleText('red', ng.id)}`,
);
}
Logger.info(`External peer ${externalPeer.id} deleted from Network Group ${ng.id}`);
Logger.info(`Unlinking parent member ${externalPeer.parentMember} from Network Group ${ng.id}`);
await unlinkMember(ngIdOrLabel, externalPeer.parentMember, org);
const checkParentMember = await checkResource(ng.id, org, externalPeer.parentMember, false);
if (!checkParentMember) {
throw new Error(
`Parent member ${styleText('red', externalPeer.parentMember)} still linked to Network Group ${styleText('red', ng.id)}`,
);
}
Logger.info(`Parent member ${externalPeer.parentMember} unlinked from Network Group ${ng.id}`);
}
/**
* Link a Member to a Network Group
* @param {object} ngIdOrLabel The Network group ID or Label
* @param {string} memberId ID of the Member to link
* @param {object} org Organisation ID or name
* @param {string} label Label of the Member
*/
export async function linkMember(ngIdOrLabel, memberId, org, label) {
if (!memberId) {
throw new Error('A valid member ID is required (addon_xxx, app_xxx, external_xxx)');
}
const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
if (!ng) {
throw new Error(`Network Group ${styleText('red', ngIdOrLabel.ngId || ngIdOrLabel.ngResourceLabel)} not found`);
}
await checkMembersToLink([memberId], ng.ownerId);
const alreadyMember = ng.members.find((m) => m.id === memberId);
if (alreadyMember) {
throw new Error(
`Member ${styleText('red', memberId)} is already linked to Network Group ${styleText('red', ng.id)}`,
);
}
const [member] = networkGroup.constructMembers(ng.id, [memberId]);
const body = {
id: member.id,
label: label || member.label,
domainName: member.domainName,
kind: member.kind,
};
Logger.info(`Linking member ${member.id} to Network Group ${ng.id}`);
Logger.debug('Sending body: ' + JSON.stringify(body, null, 2));
await networkGroupApi.createNetworkGroupMember({ ownerId: ng.ownerId, networkGroupId: ng.id }, body).then(sendToApi);
const check = await checkResource(ng.id, org, member.id, true);
if (!check) {
throw new Error(`Member ${styleText('red', member.id)} not linked to Network Group ${styleText('red', ng.id)}`);
}
Logger.info(`Member ${member.id} linked to Network Group ${ng.id}`);
}
/**
* Unlink a Member from a Network Group
* @param {object} ngIdOrLabel The Network Group ID or Label
* @param {string} memberId The Member ID
* @param {object} org Organisation ID or name
* @throws {Error} If a valid member ID is not provided
* @throws {Error} If the Network Group is not found
* @throws {Error} If the Member is not found in the Network Group
* @throws {Error} If the Member is still linked to the Network Group
*/
export async function unlinkMember(ngIdOrLabel, memberId, org) {
if (!memberId) {
throw new Error('A valid member ID is required (addon_xxx, app_xxx, external_xxx)');
}
const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
if (!ng) {
throw new Error(`Network Group ${styleText('red', ngIdOrLabel.ngId || ngIdOrLabel.ngLabel)} not found`);
}
const member = ng.members.find((m) => m.id === memberId);
if (!member) {
throw new Error(`Member ${styleText('red', memberId)} not found in Network Group ${styleText('red', ng.id)}`);
}
Logger.info(`Unlinking member ${memberId} from Network Group ${ng.id}`);
await networkGroupApi
.deleteNetworkGroupMember({ ownerId: ng.ownerId, networkGroupId: ng.id, memberId })
.then(sendToApi);
const check = await checkResource(ng.id, org, memberId, false);
if (!check) {
throw new Error(`Member ${styleText('red', memberId)} still linked to Network Group ${styleText('red', ng.id)}`);
}
Logger.info(`Member ${memberId} unlinked from Network Group ${ng.id}`);
}
/**
* Check if members can be linked to a Network Group
* @param {Array<string>} members Members to check
* @throws {Error} If members can't be linked to a Network Group
*/
export async function checkMembersToLink(members, ownerId) {
const VALID_ADDON_PROVIDERS = ['es-addon', 'mongodb-addon', 'mysql-addon', 'postgresql-addon', 'redis-addon'];
const summary = await getSummary().then(sendToApi);
let data = summary.user;
if (summary.user.id !== ownerId) {
data = summary.organisations.find((o) => o.id === ownerId);
}
const membersNotOK = [];
let source = data.applications;
for (const memberId of members) {
if (memberId.startsWith('addon_')) {
source = data.addons;
}
const foundRessource = source.find((r) => r.id === memberId);
if (foundRessource && memberId.startsWith('addon_') && !VALID_ADDON_PROVIDERS.includes(foundRessource.providerId)) {
membersNotOK.push(memberId);
} else if (!foundRessource && !memberId.startsWith('external_')) {
membersNotOK.push(memberId);
}
}
if (membersNotOK.length > 0) {
throw new Error(
`Member(s) ${styleText('red', membersNotOK.join(', '))} can't be linked to the Network Group, check Organisation ID or name`,
);
}
}
/**
* Check if a resource is present in a Network Group by ID or label
* @param {string} ngId Network Group ID
* @param {object} org Organisation ID or name
* @param {string} resource Resource ID or label
* @param {boolean} shouldBePresent Expected presence of the resource
* @param {string} [resourceType] Resource type (member or peer), default is member
* @param {string} [searchBy] Search by 'id' or 'label', default is 'id'
* @returns {Promise<boolean>} True if the resource is present, false otherwise
*/
async function checkResource(ngId, org, resource, shouldBePresent, resourceType = 'member', searchBy = 'id') {
const endTime = Date.now() + networkGroup.POLLING_TIMEOUT_MS;
while (Date.now() < endTime) {
const ng = await networkGroup.getNG(ngId, org);
const items = resourceType === 'member' ? ng.members : ng.peers;
const isPresent = items.some((item) => item[searchBy] === resource);
if (isPresent === shouldBePresent) {
return true;
}
await setTimeout(networkGroup.POLLING_INTERVAL_MS);
}
return false;
}