From e078da1509e40577a5bc78e30027c94065a82cde Mon Sep 17 00:00:00 2001 From: David Legrand Date: Sat, 26 Oct 2024 00:37:55 +0200 Subject: [PATCH] chore(ng): start refactor --- bin/clever.js | 151 +++++------ src/commands/ng-members.js | 102 ++++++++ src/commands/ng-peers.js | 161 ++++++++++++ src/commands/ng.js | 487 +++++++++--------------------------- src/models/format-string.js | 32 --- src/models/ng-peers.js | 50 ++++ src/models/ng.js | 250 ++++++++++++++++-- 7 files changed, 743 insertions(+), 490 deletions(-) create mode 100644 src/commands/ng-members.js create mode 100644 src/commands/ng-peers.js delete mode 100644 src/models/format-string.js create mode 100644 src/models/ng-peers.js diff --git a/bin/clever.js b/bin/clever.js index 7246f53..2fac1cf 100755 --- a/bin/clever.js +++ b/bin/clever.js @@ -9,7 +9,6 @@ import colors from 'colors/safe.js'; import _sortBy from 'lodash/sortBy.js'; import { getPackageJson } from '../src/load-package-json.cjs'; -import * as Formatter from '../src/models/format-string.js'; import * as git from '../src/models/git.js'; import * as Parsers from '../src/parsers.js'; import { handleCommandPromise } from '../src/command-promise-handler.js'; @@ -46,6 +45,8 @@ import * as logout from '../src/commands/logout.js'; import * as logs from '../src/commands/logs.js'; import * as makeDefault from '../src/commands/makeDefault.js'; import * as ng from '../src/commands/ng.js'; +import * as ngMembers from '../src/commands/ng-members.js'; +import * as ngPeers from '../src/commands/ng-peers.js'; import * as notifyEmail from '../src/commands/notify-email.js'; import * as open from '../src/commands/open.js'; import * as consoleModule from '../src/commands/console.js'; @@ -102,13 +103,12 @@ async function run () { ngIdOrLabel: cliparse.argument('ng', { description: 'Network Group ID or label', parser: Parsers.ngIdOrLabel, - // complete: NetworkGroup('xxx'), }), ngDescription: cliparse.argument('description', { description: 'Network Group description', }), ngMemberId: cliparse.argument('member-id', { - description: `The member ID: an app ID (e.g.: ${Formatter.formatCode('app_xxx')}), add-on ID (e.g.: ${Formatter.formatCode('addon_xxx')}) or external node category ID`, + description: 'The member ID: an app ID (e.g.: \'app_xxx\'), add-on ID (e.g.: \'addon_xxx\') or external node category ID', // complete: NetworkGroup('xxx'), }), ngMembersIds: cliparse.argument('members-ids', { @@ -116,20 +116,20 @@ async function run () { parser: Parsers.commaSeparated, }), ngMemberDomainName: cliparse.argument('domain-name', { - description: `Member name used in the ${Formatter.formatUrl('.members..ng.clever-cloud.com', false)} domain name alias`, + description: 'Member name used in the \'.members..ng.clever-cloud.com\', false domain name alias', }), ngPeerId: cliparse.argument('peer-id', { description: 'The peer ID', // complete: NetworkGroup('xxx'), }), ngPeerRole: cliparse.argument('role', { - description: `The peer role, (${Formatter.formatString('client')} or ${Formatter.formatString('server')})`, + description: `The peer role, (${'client'} or ${'server'})`, parser: Parsers.ngPeerRole, complete: NetworkGroup.listAvailablePeerRoles, }), // FIXME: Add "internal" member type ngMemberType: cliparse.argument('type', { - description: `The member type (${Formatter.formatString('application')}, ${Formatter.formatString('addon')} or ${Formatter.formatString('external')})`, + description: `The member type (${'application'}, ${'addon'} or ${'external'})`, parser: Parsers.ngMemberType, complete: NetworkGroup.listAvailableMemberTypes, }), @@ -202,6 +202,12 @@ async function run () { // OPTIONS const opts = { // Network Groups options + ngIdOrLabel: cliparse.option('ng', { + metavar: 'ng_id_or_label', + description: 'Network Group ID or label', + parser: Parsers.ngIdOrLabel, + // complete: NetworkGroup('xxx'), + }), ngMembersIds: cliparse.option('members-ids', { metavar: 'members_ids', description: "Comma separated list of Network Group members IDs ('app_xxx', 'addon_xxx', 'external_xxx')", @@ -216,6 +222,9 @@ async function run () { metavar: 'member_label', description: 'The member label', }), + ngPeerGetConfig: cliparse.flag('config', { + description: 'Get the Wireguard configuration of an external node', + }), wgPublicKey: cliparse.option('public-key', { required: false, metavar: 'public_key', @@ -812,85 +821,83 @@ async function run () { args: [args.alias], }, makeDefault.makeDefault); - // NETWORK GROUP COMMANDS - const networkGroupsCreateCommand = cliparse.command('create', { - description: 'Create a Network Group', - args: [args.ngLabel], - options: [opts.ngMembersIds, opts.ngDescription, opts.optTags, opts.humanJsonOutputFormat], - }, ng.createNg); - const networkGroupsDeleteCommand = cliparse.command('delete', { - description: 'Delete a Network Group', - args: [args.ngIdOrLabel], - options: [opts.humanJsonOutputFormat], - }, ng.deleteNg); - const networkGroupsListCommand = cliparse.command('list', { - description: 'List available Network Groups with their labels', - options: [opts.humanJsonOutputFormat], - }, ng.listNg); - const networkGroupsGetCommand = cliparse.command('get', { - description: 'Get details about a Network Group', - args: [args.ngIdOrLabel], - options: [opts.humanJsonOutputFormat], - }, ng.getNg); - - const networkGroupsMemberListCommand = cliparse.command('list', { - description: 'List members of a Network Group', - // Add option opts.optNgSearchAppId ? - args: [args.ngIdOrLabel], - options: [opts.naturalName, opts.humanJsonOutputFormat], - }, ng.listMembers); - const networkGroupsMemberGetCommand = cliparse.command('get', { - description: 'Get a Network Group member\'s details', - args: [args.ngIdOrLabel, args.ngMemberId], - options: [opts.naturalName, opts.humanJsonOutputFormat], - }, ng.getMember); - const networkGroupsMemberAddCommand = cliparse.command('add', { + // NETWORK GROUPS ADD COMMANDS + const ngMembersAddCommand = cliparse.command('member', { description: 'Add an app or add-on as a Network Group member', args: [args.ngIdOrLabel, args.ngMemberId], options: [opts.ngMemberLabel], - }, ng.addMember); - const networkGroupsMemberRemoveCommand = cliparse.command('remove', { + }, ngMembers.addMember); + const ngMembersAddExternalCommand = cliparse.command('external-peer', { + description: 'Add an external node as a Network Group peer', + args: [args.ngIdOrLabel, args.ngPeerLabel, args.ngPeerRole, args.ngPeerParentMemberId], + options: [opts.humanJsonOutputFormat, opts.wgPublicKey], + }, ngPeers.addExternalPeer); + + // NETWORK GROUPS REMOVE COMMANDS + const ngMembersRemoveCommand = cliparse.command('member', { description: 'Remove an app or add-on from a Network Group', - args: [args.ngIdOrLabel, args.ngMemberId], - }, ng.removeMember); - const networkGroupsMembersCategoryCommand = cliparse.command('members', { - description: 'List commands for interacting with Network Group members', - commands: [networkGroupsMemberListCommand, networkGroupsMemberGetCommand, networkGroupsMemberAddCommand, networkGroupsMemberRemoveCommand], - }, ng.listMembers); + args: [args.ngMemberId], + options: [opts.ngIdOrLabel], + }, ngMembers.removeMember); + const ngPeersRemoveCommand = cliparse.command('peer', { + description: 'Remove an external node from a Network Group', + args: [args.ngPeerId], + options: [opts.ngIdOrLabel], + }, ngPeers.removeExternalPeer); - const networkGroupsPeerListCommand = cliparse.command('list', { + // NETWORK GROUPS LIST COMMANDS + const ngMembersListCommand = cliparse.command('members', { + description: 'List members of a Network Group', + args: [args.ngIdOrLabel], + }, ngMembers.listMembers); + const ngPeersListCommand = cliparse.command('peers', { description: 'List peers of a Network Group', args: [args.ngIdOrLabel], - options: [opts.humanJsonOutputFormat], - }, ng.listPeers); - const networkGroupsPeerGetCommand = cliparse.command('get', { + }, ngPeers.listPeers); + + // NETWORK GROUPS GET COMMANDS + const ngMembersGetCommand = cliparse.command('member', { + description: 'Get a Network Group member\'s details', + args: [args.ngMemberId], + options: [opts.naturalName, opts.ngIdOrLabel], + }, ngMembers.getMember); + const ngPeersGetCommand = cliparse.command('peer', { description: 'Get a Network Group peer\'s details', - args: [args.ngIdOrLabel, args.ngPeerId], + args: [args.ngPeerId], + options: [opts.ngPeerGetConfig, opts.ngIdOrLabel], + }, ngPeers.getPeer); + + // NETWORK GROUP COMMANDS + const ngListCommand = cliparse.command('list', { + description: 'List Network Groups (default), their members or peers', options: [opts.humanJsonOutputFormat], - }, ng.getPeer); - const networkGroupsPeerAddExternalCommand = cliparse.command('add-external', { - description: 'Add an external node as a Network Group peer', - args: [args.ngIdOrLabel, args.ngPeerLabel, args.ngPeerRole, args.ngPeerParentMemberId], - options: [opts.humanJsonOutputFormat, opts.wgPublicKey], - }, ng.addExternalPeer); - const networkGroupsPeerRemoveExternalCommand = cliparse.command('remove-external', { - description: 'Remove an external node from a Network Group', - args: [args.ngIdOrLabel, args.ngPeerId], - }, ng.removeExternalPeer); - const networkGroupsPeerGetConfigCommand = cliparse.command('get-config', { - description: 'Get the configuration of an external node', - args: [args.ngIdOrLabel, args.ngPeerId], + commands: [ngMembersListCommand, ngPeersListCommand], + }, ng.listNg); + const ngCreateCommand = cliparse.command('add', { + description: 'Add a Network Group (default), a member or an external peer', + args: [args.ngLabel], + privateOptions: [opts.ngMembersIds, opts.ngDescription, opts.optTags], + commands: [ngMembersAddCommand, ngMembersAddExternalCommand], + }, ng.createNg); + const ngDeleteCommand = cliparse.command('remove', { + description: 'Delete a Network Group (default), a member or an external peer', + args: [args.ngIdOrLabel], + commands: [ngMembersRemoveCommand, ngPeersRemoveCommand], + }, ng.deleteNg); + const ngGetCommand = cliparse.command('get', { + description: 'Get details about a Network Group (default), a member or a peer', + args: [args.ngIdOrLabel], options: [opts.humanJsonOutputFormat], - }, ng.getExternalPeerConfig); - const networkGroupsPeersCategoryCommand = cliparse.command('peers', { - description: 'List commands for interacting with Network Group peers', - commands: [networkGroupsPeerListCommand, networkGroupsPeerGetCommand, networkGroupsPeerAddExternalCommand, networkGroupsPeerRemoveExternalCommand, networkGroupsPeerGetConfigCommand], - }, ng.listPeers); - + commands: [ngMembersGetCommand, ngPeersGetCommand], + }, ng.getNg); + const ngJoinCommand = cliparse.command('join', { + description: 'Join a Network Group (default), a member or an external peer', + args: [args.ngIdOrLabel], + }, ng.joinNg); const networkGroupsCommand = cliparse.command('ng', { description: 'Manage Network Groups, their members and peers', options: [opts.orgaIdOrName], - commands: [networkGroupsCreateCommand, networkGroupsDeleteCommand, networkGroupsListCommand, networkGroupsGetCommand, networkGroupsMembersCategoryCommand, networkGroupsPeersCategoryCommand], + commands: [ngListCommand, ngCreateCommand, ngDeleteCommand, ngGetCommand, ngJoinCommand], }, ng.listNg); // NOTIFY-EMAIL COMMAND diff --git a/src/commands/ng-members.js b/src/commands/ng-members.js new file mode 100644 index 0000000..2f4691a --- /dev/null +++ b/src/commands/ng-members.js @@ -0,0 +1,102 @@ +import { Logger } from '../logger.js'; +import * as ng from '../models/ng.js'; +import * as ngApi from '@clevercloud/client/cjs/api/v4/network-group.js'; +import { sendToApi } from '../models/send-to-api.js'; + +export async function listMembers (params) { + const [networkGroupIdOrLabel] = params.args; + const { org: orgaIdOrName, 'natural-name': naturalName, format } = params.options; + + const ownerId = await ng.getOwnerId(orgaIdOrName); + const networkGroupId = await ng.getId(ownerId, networkGroupIdOrLabel); + + Logger.info(`Listing members from Network Group '${networkGroupId}'`); + Logger.info(naturalName); + + const result = await ngApi.listNetworkGroupMembers({ ownerId, networkGroupId }).then(sendToApi); + Logger.debug(`Received from API: ${JSON.stringify(result, null, 2)}`); + + switch (format) { + case 'json': { + Logger.println(JSON.stringify(result, null, 2)); + break; + } + case 'human': + default: { + if (result.length === 0) { + Logger.println('No member found. You can add one with \'clever ng members add\'.'); + } + else { + const domainNames = result.map((item) => ({ domainName: item.domainName })); + console.table(domainNames); + } + } + } +} + +export async function getMember (params) { + const [networkGroupIdOrLabel, memberId] = params.args; + const { org: orgaIdOrName, 'natural-name': naturalName, format } = params.options; + + const ownerId = await ng.getOwnerId(orgaIdOrName); + const networkGroupId = await ng.getId(ownerId, networkGroupIdOrLabel); + + Logger.info(`Getting details for member ${memberId} in Network Group ${networkGroupId}`); + Logger.info(`Natural name: ${naturalName}`); + const result = await ngApi.getNetworkGroupMember({ ownerId, networkGroupId, memberId: memberId }).then(sendToApi); + Logger.debug(`Received from API: ${JSON.stringify(result, null, 2)}`); + + switch (format) { + case 'json': { + Logger.println(JSON.stringify(result, null, 2)); + break; + } + case 'human': + default: { + const domainName = [result].map((item) => ({ domainName: item.domainName })); + console.table(domainName); + } + } +} + +export async function addMember (params) { + const [networkGroupIdOrLabel, memberId] = params.args; + const { org: orgaIdOrName, label } = params.options; + + const ownerId = await ng.getOwnerId(orgaIdOrName); + const networkGroupId = await ng.getId(ownerId, networkGroupIdOrLabel); + const domainName = `${memberId}.m.${networkGroupId}.ng.clever-cloud.com`; + + let type = null; + if (memberId.startsWith('app_')) { + type = 'application'; + } + else if (memberId.startsWith('addon_')) { + type = 'addon'; + } + else if (memberId.startsWith('external_')) { + type = 'external'; + } + else { + // throw new Error(`Member ID ${Formatter.formatString(memberId)} is not a valid format. It should start with 'app_', 'addon_' or 'external_'`); + type = 'addon'; + } + + const body = { id: memberId, label, domainName: domainName, type }; + Logger.debug('Sending body: ' + JSON.stringify(body, null, 2)); + await ngApi.createNetworkGroupMember({ ownerId, networkGroupId }, body).then(sendToApi); + + Logger.println(`Successfully added member ${memberId} to Network Group ${networkGroupId}.`); +} + +export async function removeMember (params) { + const [networkGroupIdOrLabel, memberId] = params.args; + const { org: orgaIdOrName } = params.options; + + const ownerId = await ng.getOwnerId(orgaIdOrName); + const networkGroupId = await ng.getId(ownerId, networkGroupIdOrLabel); + + await ngApi.deleteNetworkGroupMember({ ownerId, networkGroupId, memberId }).then(sendToApi); + + Logger.println(`Successfully removed member ${memberId} from Network Group ${networkGroupId}.`); +} diff --git a/src/commands/ng-peers.js b/src/commands/ng-peers.js new file mode 100644 index 0000000..ad81dd1 --- /dev/null +++ b/src/commands/ng-peers.js @@ -0,0 +1,161 @@ +import { randomBytes } from 'crypto'; +import { Logger } from '../logger.js'; +import * as NG from '../models/ng.js'; +import * as Peers from '../models/ng-peers.js'; +import * as ngApi from '@clevercloud/client/cjs/api/v4/network-group.js'; +import { sendToApi } from '../models/send-to-api.js'; + +export async function listPeers (params) { + const [ngIdOrLabel] = params.args; + const { org, format } = params.options; + + const networkGroupId = await NG.getId(org, ngIdOrLabel); + const peers = await Peers.list(org, networkGroupId); + + switch (format) { + case 'json': { + Logger.println(JSON.stringify(peers, null, 2)); + break; + } + case 'human': + default: { + if (peers.length === 0) { + Logger.println('No peer found. You can add an external one with \'clever ng peers add-external\'.'); + } + else { + for (const peer of peers) { + if (peer.endpoint.ngTerm && peer.endpoint.publicTerm) { + peer.ngTerm = `${peer.endpoint.ngTerm.host}:${peer.endpoint.ngTerm.port}`; + peer.publicTerm = `${peer.endpoint.publicTerm.host}:${peer.endpoint.publicTerm.port}`; + delete peer.endpoint; + } + else { + peer.ngIp = peer.endpoint.ngIp; + peer.type = peer.endpoint.type; + delete peer.endpoint; + } + + Logger.println(); + Logger.println(`Peer ${peer.id}:`); + delete peer.id; + + console.table(peer); + } + } + } + } +} + +export async function getPeer (params) { + const [peerId] = params.args; + const { ng, org, format } = params.options; + const { config } = params.options; + + const networkGroupId = await NG.getId(org, ng); + const peer = await Peers.get(org, networkGroupId, peerId); + + switch (format) { + case 'json': { + Logger.println(JSON.stringify(peer, null, 2)); + break; + } + case 'human': + default: { + // We keep only id, label, 'host:ip': `${endpoint.ngTerm.host}:${endpoint.ngTerm.port}`, type + const peerList = { id: peer.id, label: peer.label, 'host:ip': `${peer.endpoint.ngTerm.host}:${peer.endpoint.ngTerm.port}`, type: peer.type }; + console.table([peerList]); + } + } +} + +export async function addExternalPeer (params) { + const { org: orgaIdOrName, format, 'public-key': publicKey } = params.options; + const [networkGroupIdOrLabel, label, role, parent] = params.args; + + const ownerId = await NG.getOwnerId(orgaIdOrName); + const networkGroupId = await NG.getId(ownerId, networkGroupIdOrLabel); + + const pk = publicKey || randomBytes(31).toString('base64').replace(/\//g, '-').replace(/\+/g, '_').replace(/=/g, ''); + const body = { peerRole: role, publicKey: pk, label, parentMember: parent }; + // Optional parameters: ip, port, hostname, parentEvent + Logger.info(`Adding external peer to Network Group ${networkGroupId}`); + Logger.debug('Sending body: ' + JSON.stringify(body, null, 2)); + const result = await ngApi.createNetworkGroupExternalPeer({ ownerId, networkGroupId }, body).then(sendToApi); + Logger.debug(`Received from API: ${JSON.stringify(result, null, 2)}`); + + switch (format) { + case 'json': { + Logger.println(JSON.stringify(result, null, 2)); + break; + } + case 'human': + default: { + Logger.println(`External peer ${result.peerId} have been added to Network Group ${networkGroupId}`); + } + } +} + +export async function removeExternalPeer (params) { + const { org: orgaIdOrName } = params.options; + const [networkGroupIdOrLabel, peerId] = params.args; + + const ownerId = await NG.getOwnerId(orgaIdOrName); + const networkGroupId = await NG.getId(ownerId, networkGroupIdOrLabel); + + Logger.info(`Removing external peer ${peerId} from Network Group ${networkGroupId}`); + await ngApi.deleteNetworkGroupExternalPeer({ ownerId, networkGroupId, peerId }).then(sendToApi); + + Logger.println(`External peer ${peerId} have been removed from Network Group ${networkGroupId}`); +} + +export async function getExternalPeerConfig (params) { + const { org: orgaIdOrName, format } = params.options; + const [networkGroupIdOrLabel, peerId] = params.args; + const { config } = params.options; + console.log(config); + const ownerId = await NG.getOwnerId(orgaIdOrName); + const networkGroupId = await NG.getId(ownerId, networkGroupIdOrLabel); + + Logger.info(`Getting external peer config ${peerId} from Network Group ${networkGroupId}`); + const result = await ngApi.getNetworkGroupWireGuardConfiguration({ ownerId, networkGroupId, peerId }).then(sendToApi); + Logger.debug(`Received from API: ${JSON.stringify(result, null, 2)}`); + + const peerToPrint = result.peers.find((peer) => peer.peer_id === peerId); + result.configuration = Buffer.from(result.configuration, 'base64').toString(); + console.log(result.configuration); + switch (format) { + case 'json': { + Logger.println(JSON.stringify(result, null, 2)); + break; + } + case 'human': + default: { + Logger.info(`Peer: ${peerId} (${peerToPrint.peer_ip})`); + Logger.info(`Host: ${peerToPrint.peer_hostname}`); + + Logger.println(result.configuration); + /* Logger.println(`Peer ${peerId)}:`); + /* Logger.println(`Peer ${peerId)}:`); + Logger.println(` - ${peerToPrint.peer_id} (${peerToPrint.peer_ip})`); + Logger.println(` - ${peerToPrint.peer_hostname}`); + Logger.println(); + Logger.println(`Configuration: ${result.configuration}`); + Logger.println(`Configuration: ${result.configuration}`); + Logger.println(result.configuration); + + // we set the configuration in a temp file and use 'wg-quick' system tool to apply it + /* const tmpFile = path.join(tmpdir(), `wg${Date.now()}.conf`); + fs.writeFileSync(tmpFile, result.configuration.replace('<%PrivateKey%>', 'PVK')); + console.log(tmpFile); + console.log(fs.readFileSync(tmpFile)); + await execSync(`wg-quick up ${tmpFile}`); + fs.unlinkSync(tmpFile); + Logger.println('Peer configuration applied'); + */ + break; + } + } +} + +// https://api.clever-cloud.com/v4/networkgroups/organisations/user_2e58efd9-f849-4d14-93cf-e0110a22630c/networkgroups/ng_43616667-13a8-4f53-a6c5-4d735b133de2/peers/external_f16b3f83-4c50-4602-8cb6-adfe79e95342/wireguard/configuration +// https://api.clever-cloud.com/v4/networkgroups/organisations/orga_2eb942c9-ae24-40fe-9e4c-53c9982a02b1/networkgroups/ng_d87aa48b-bb01-4902-9c3c-e8d111a4b3c5/peers/external_88547661-d63c-4f06-8fb0-d61ad0d4733c/wireguard/configuration diff --git a/src/commands/ng.js b/src/commands/ng.js index da70ed3..708931b 100644 --- a/src/commands/ng.js +++ b/src/commands/ng.js @@ -1,415 +1,166 @@ -import { randomBytes } from 'crypto'; +import colors from 'colors/safe.js'; +import * as NG from '../models/ng.js'; import { Logger } from '../logger.js'; -import * as NetworkGroup from '../models/ng.js'; -import * as Formatter from '../models/format-string.js'; -import * as ngApi from '@clevercloud/client/cjs/api/v4/network-group.js'; - -import { v4 as uuidv4 } from 'uuid'; -import { sendToApi } from '../models/send-to-api.js'; - -const TIMEOUT = 5000; -const INTERVAL = 500; - -export async function listNg (params) { - const { org: orgaIdOrName, format } = params.options; - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - - Logger.info(`Listing Network Groups from owner ${Formatter.formatString(ownerId)}`); - const result = await ngApi.listNetworkGroups({ ownerId }).then(sendToApi); - Logger.debug(`Received from API: ${JSON.stringify(result, null, 2)}`); - - if (result.length === 0) { - Logger.println(`No Network Group found for ${ownerId}`); - Logger.println(`You can create one with ${Formatter.formatCommand('clever networkgroups create')} command`); - return; - } - - switch (format) { - case 'json': { - Logger.println(JSON.stringify(result, null, 2)); - break; - } - case 'human': - default: { - // We keep only id, label, networkIp, lastAllocatedIp - const ngList = result.map(({ - id, label, networkIp, lastAllocatedIp, members, peers, - }) => ({ - id, label, networkIp, lastAllocatedIp, members: Object.keys(members).length, peers: Object.keys(peers).length, - })); - - console.table(ngList); - } - } -} - -export async function getNg (params) { - const [networkGroupIdOrLabel] = params.args; - const { org: orgaIdOrName, format } = params.options; - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); - const result = await ngApi.getNetworkGroup({ ownerId, networkGroupId }).then(sendToApi); - Logger.debug(`Received from API: ${JSON.stringify(result, null, 2)}`); - - switch (format) { - case 'json': { - Logger.println(JSON.stringify(result, null, 2)); - break; - } - case 'human': - default: { - const ngData = { - id: result.id, - label: result.label, - description: result.description, - network: `${result.networkIp}`, - 'members/peers': `${Object.keys(result.members).length}/${Object.keys(result.peers).length}`, - }; - - console.table(ngData); - Logger.println(); - - Logger.println('Members:'); - const members = Object.entries(result.members).map(([id, member]) => ({ domainName: member.domainName })); - console.table(members); - - Logger.println('Peers:'); - const peers = Object.entries(result.peers).map(([id, peer]) => ({ parent: peer.parentMember, id: peer.id, label: peer.label, IP: peer.endpoint.ngTerm.host, publicKey: peer.publicKey })); - console.table(peers); - } - } -} - +import { execSync } from 'node:child_process'; + +/** Create a Network Group + * @param {Object} params + * @param {string} params.args[0] Network Group label + * @param {string} params.options.org Organisation ID or name + * @param {string} params.options.description Network Group description + * @param {string} params.options.tags Comma-separated list of tags + * @param {string} params.options.members-ids Comma-separated list of members IDs + */ export async function createNg (params) { - const [label] = params.args; - const { org: orgaIdOrName, description, tags, format, 'members-ids': members_ids } = params.options; - - // We generate and set a unique ID to know it before the API call and reuse it later - const ngId = `ng_${uuidv4()}`; - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - - let members = []; - if (members_ids) { - // For each member ID, we add a type depending on the ID format and a domain name - members = members_ids.map((id) => { - - const domainName = `${id}.m.${ngId}.ng.clever-cloud.com`; - const prefixToType = { - app_: 'application', - addon_: 'addon', - external_: 'external', - }; - - const prefix = Object.keys(prefixToType) - .find((p) => id.startsWith(p)); + const [ngLabel] = params.args; + const { org, description, 'members-ids': membersIds, tags } = params.options; - let type = prefixToType[prefix]; - if (!type) { - // throw new Error(`Member ID ${Formatter.formatString(id)} is not a valid format. It should start with 'app_', 'addon_' or 'external_'`); - type = 'addon'; - } - return { id, domainName, type }; - }); - } - - const body = { ownerId: ownerId, id: ngId, label, description, tags, members }; - Logger.info(`Creating Network Group ${Formatter.formatString(label)} (${Formatter.formatId(ngId)}) from owner ${Formatter.formatString(ownerId)}`); - Logger.info(`${members.length} members will be added: ${members.map((m) => Formatter.formatString(m.id)).join(', ')}`); - Logger.debug(`Sending body: ${JSON.stringify(body, null, 2)}`); - await ngApi.createNetworkGroup({ ownerId }, body).then(sendToApi); - - // We poll until NG is created to display the result - const polling = setInterval(async () => { - const ng = await ngApi.getNetworkGroup({ ownerId, networkGroupId: ngId }).then(sendToApi).catch(() => { - Logger.error(`Error while fetching Network Group ${Formatter.formatString(ngId)}`); - process.exit(1); - }); - - Logger.debug(`Received from API during polling: ${JSON.stringify(ng, null, 2)}`); - - if (ng.label === label) { - clearInterval(polling); - clearTimeout(timeout); - - const message = format === 'json' - ? JSON.stringify(ng, null, 2) - : `Network Group ${Formatter.formatString(label)} (${Formatter.formatId(ngId)}) has been created successfully`; - - Logger.println(message); - } - }, INTERVAL); - - const timeout = setTimeout(() => { - clearInterval(polling); - Logger.error('Network group creation has been launched asynchronously but timed out. Check the status later with `clever ng list`.'); - }, TIMEOUT); + const { id: ngId, ownerId } = await NG.create(org, ngLabel, description, tags, membersIds); + NG.pollNewNetworkGroup(ownerId, ngId, membersIds); } +/** Delete a Network Group + * @param {Object} params + * @param {string} params.args[0] Network Group ID or label + * @param {string} params.options.org Organisation ID or name + */ export async function deleteNg (params) { - const [networkGroupIdOrLabel] = params.args; - const { org: orgaIdOrName } = params.options; - - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); - - Logger.info(`Deleting Network Group ${Formatter.formatString(networkGroupId)} from owner ${Formatter.formatString(ownerId)}`); - await ngApi.deleteNetworkGroup({ ownerId, networkGroupId }).then(sendToApi); - - // We poll until NG is deleted to display the result - const polling = setInterval(async () => { - const ngList = await ngApi.listNetworkGroups({ ownerId }).then(sendToApi); - const ng = ngList.find((ng) => ng.id === networkGroupId); - - Logger.debug(`Received from API during polling: ${JSON.stringify(ng, null, 2)}`); + const [ngIdOrLabel] = params.args; + const { org } = params.options; - if (!ng) { - clearInterval(polling); - clearTimeout(timeout); - - Logger.println(`Network Group ${Formatter.formatString(networkGroupId)} has been deleted successfully`); - } - }, INTERVAL); - - const timeout = setTimeout(() => { - clearInterval(polling); - Logger.error('Network group deletion has been launched asynchronously but timed out. Check the status later with `clever ng list`.'); - }, TIMEOUT); + const { id: ngId, ownerId } = await NG.destroy(ngIdOrLabel, org); + NG.pollDeletedNetworkGroup(ownerId, ngId); } -export async function listMembers (params) { - const [networkGroupIdOrLabel] = params.args; - const { org: orgaIdOrName, 'natural-name': naturalName, format } = params.options; - - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); - - Logger.info(`Listing members from Network Group '${networkGroupId}'`); - Logger.info(naturalName); - - const result = await ngApi.listNetworkGroupMembers({ ownerId, networkGroupId }).then(sendToApi); - Logger.debug(`Received from API: ${JSON.stringify(result, null, 2)}`); - - switch (format) { - case 'json': { - Logger.println(JSON.stringify(result, null, 2)); - break; - } - case 'human': - default: { - if (result.length === 0) { - Logger.println(`No member found. You can add one with ${Formatter.formatCommand('clever networkgroups members add')}.`); - } - else { - const domainNames = result.map((item) => ({ domainName: item.domainName })); - console.table(domainNames); - } - } +export async function joinNg (params) { + try { + const cmd = process.platform === 'win32' ? 'where wg-quick' : 'which wg-quick'; + execSync(cmd, { stdio: 'ignore' }); + Logger.println('wg-quick is available'); } -} - -export async function getMember (params) { - const [networkGroupIdOrLabel, memberId] = params.args; - const { org: orgaIdOrName, 'natural-name': naturalName, format } = params.options; - - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); - - Logger.info(`Getting details for member ${Formatter.formatString(memberId)} in Network Group ${Formatter.formatString(networkGroupId)}`); - Logger.info(`Natural name: ${naturalName}`); - const result = await ngApi.getNetworkGroupMember({ ownerId, networkGroupId, memberId: memberId }).then(sendToApi); - Logger.debug(`Received from API: ${JSON.stringify(result, null, 2)}`); - - switch (format) { - case 'json': { - Logger.println(JSON.stringify(result, null, 2)); - break; - } - case 'human': - default: { - const domainName = [result].map((item) => ({ domainName: item.domainName })); - console.table(domainName); - } + catch (error) { + Logger.error('wg-quick is not available, please install WireGuard'); + return; } -} -export async function addMember (params) { - const [networkGroupIdOrLabel, memberId] = params.args; - const { org: orgaIdOrName, label } = params.options; + const [ngIdOrLabel] = params.args; + const { org } = params.options; - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); - const domainName = `${memberId}.m.${networkGroupId}.ng.clever-cloud.com`; + const ngId = await NG.getId(org, ngIdOrLabel); - let type = null; - if (memberId.startsWith('app_')) { - type = 'application'; - } - else if (memberId.startsWith('addon_')) { - type = 'addon'; - } - else if (memberId.startsWith('external_')) { - type = 'external'; - } - else { - // throw new Error(`Member ID ${Formatter.formatString(memberId)} is not a valid format. It should start with 'app_', 'addon_' or 'external_'`); - type = 'addon'; + if (!ngId) { + Logger.println(`Network Group ${colors.green(ngIdOrLabel.ngLabel || ngIdOrLabel.ngId)} not found`); + return; } - const body = { id: memberId, label, domainName: domainName, type }; - Logger.debug('Sending body: ' + JSON.stringify(body, null, 2)); - await ngApi.createNetworkGroupMember({ ownerId, networkGroupId }, body).then(sendToApi); + Logger.println(`Joining Network Group ${colors.green(ngIdOrLabel.ngLabel || ngIdOrLabel.ngId)}`); + /* const [ngIdOrLabel] = params.args; + const { org } = params.options; - Logger.println(`Successfully added member ${Formatter.formatString(memberId)} to Network Group ${Formatter.formatString(networkGroupId)}.`); + const ownerId = (org != null) + ? await Organisation.getId(org) + : await User.getCurrentId(); } - -export async function removeMember (params) { - const [networkGroupIdOrLabel, memberId] = params.args; - const { org: orgaIdOrName } = params.options; - - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); - - await ngApi.deleteNetworkGroupMember({ ownerId, networkGroupId, memberId }).then(sendToApi); - - Logger.println(`Successfully removed member ${Formatter.formatString(memberId)} from Network Group ${Formatter.formatString(networkGroupId)}.`); + const configuration = Peer.getConfiguration(); + // we set the configuration in a temp file and use 'wg-quick' system tool to apply it + const tmpFile = fs.path.join(fs.tmpdir(), `wg${Date.now()}.conf`); + fs.writeFileSync(tmpFile, result.configuration.replace('<%PrivateKey%>', 'OHN2iZviEWPo7S4kswHKCGgnwCCNhq38KKp3yYWwHl4=')); + console.log(tmpFile); + console.log(fs.readFileSync(tmpFile)); + await fs.execSync(`wg-quick up ${tmpFile}`); + fs.unlinkSync(tmpFile); + Logger.println('Peer configuration applied'); */ } -export async function listPeers (params) { - const [networkGroupIdOrLabel] = params.args; - const { org: orgaIdOrName, format } = params.options; - - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); - - Logger.info(`Listing peers from Network Group ${Formatter.formatString(networkGroupId)}`); - const result = await ngApi.listNetworkGroupPeers({ ownerId, networkGroupId }).then(sendToApi); - Logger.debug(`Received from API: ${JSON.stringify(result, null, 2)}`); - - switch (format) { - case 'json': { - Logger.println(JSON.stringify(result, null, 2)); - break; - } - case 'human': - default: { - if (result.length === 0) { - Logger.println(`No peer found. You can add an external one with ${Formatter.formatCommand('clever networkgroups peers add-external')}.`); - } - else { - for (const peer of result) { - if (peer.endpoint.ngTerm && peer.endpoint.publicTerm) { - peer.ngTerm = `${peer.endpoint.ngTerm.host}:${peer.endpoint.ngTerm.port}`; - peer.publicTerm = `${peer.endpoint.publicTerm.host}:${peer.endpoint.publicTerm.port}`; - delete peer.endpoint; - } - else { - peer.ngIp = peer.endpoint.ngIp; - peer.type = peer.endpoint.type; - delete peer.endpoint; - } - - Logger.println(); - Logger.println(`Peer ${Formatter.formatString(peer.id)}:`); - delete peer.id; +/** List Network Groups, their members and peers + * @param {Object} params + * @param {string} params.options.orgaIdOrName Organisation ID or name + * @param {string} params.options.format Output format + */ +export async function listNg (params) { + const { org, format } = params.options; + const ngs = await NG.getNGs(org); - console.table(peer); - } - } - } + if (!ngs.length) { + Logger.println(`No Network Group found, create one with ${colors.green('clever ng create')} command`); + return; } -} - -export async function getPeer (params) { - const [networkGroupIdOrLabel, peerId] = params.args; - const { org: orgaIdOrName, format } = params.options; - - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); - - Logger.info(`Getting details for peer ${Formatter.formatString(peerId)} in Network Group ${Formatter.formatString(networkGroupId)}`); - const peer = await ngApi.getNetworkGroupPeer({ ownerId, networkGroupId, peerId }).then(sendToApi); - Logger.debug(`Received from API: ${JSON.stringify(peer, null, 2)}`); switch (format) { case 'json': { - Logger.println(JSON.stringify(peer, null, 2)); + Logger.printJson(ngs); break; } case 'human': default: { - // We keep only id, label, 'host:ip': `${endpoint.ngTerm.host}:${endpoint.ngTerm.port}`, type - const peerList = { id: peer.id, label: peer.label, 'host:ip': `${peer.endpoint.ngTerm.host}:${peer.endpoint.ngTerm.port}`, type: peer.type }; - console.table([peerList]); + const ngList = ngs.map(({ + id, + label, + networkIp, + lastAllocatedIp, + members, + peers, + }) => ({ + id, + label, + networkIp, + lastAllocatedIp, + members: Object.keys(members).length, + peers: Object.keys(peers).length, + })); + + console.table(ngList); } } } -export async function addExternalPeer (params) { - const { org: orgaIdOrName, format, 'public-key': publicKey } = params.options; - const [networkGroupIdOrLabel, label, role, parent] = params.args; - - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); +/** Get a Network Group + * @param {Object} params + * @param {string} params.args[0] Network Group ID or label + * @param {string} params.options.org Organisation ID or name + * @param {string} params.options.format Output format + */ +export async function getNg (params) { + const [ngIdOrLabel] = params.args; + const { org, format } = params.options; - const pk = publicKey || randomBytes(31).toString('base64').replace(/\//g, '-').replace(/\+/g, '_').replace(/=/g, ''); - const body = { peerRole: role, publicKey: pk, label, parentMember: parent }; - // Optional parameters: ip, port, hostname, parentEvent - Logger.info(`Adding external peer to Network Group ${Formatter.formatString(networkGroupId)}`); - Logger.debug('Sending body: ' + JSON.stringify(body, null, 2)); - const result = await ngApi.createNetworkGroupExternalPeer({ ownerId, networkGroupId }, body).then(sendToApi); - Logger.debug(`Received from API: ${JSON.stringify(result, null, 2)}`); + const ng = await NG.get(ngIdOrLabel, org); switch (format) { case 'json': { - Logger.println(JSON.stringify(result, null, 2)); + Logger.printJson(ng); break; } case 'human': default: { - Logger.println(`External peer ${Formatter.formatString(result.peerId)} have been added to Network Group ${Formatter.formatString(networkGroupId)}`); - } - } -} - -export async function removeExternalPeer (params) { - const { org: orgaIdOrName } = params.options; - const [networkGroupIdOrLabel, peerId] = params.args; - - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); - - Logger.info(`Removing external peer ${Formatter.formatString(peerId)} from Network Group ${Formatter.formatString(networkGroupId)}`); - await ngApi.deleteNetworkGroupExternalPeer({ ownerId, networkGroupId, peerId }).then(sendToApi); - - Logger.println(`External peer ${Formatter.formatString(peerId)} have been removed from Network Group ${Formatter.formatString(networkGroupId)}`); -} - -export async function getExternalPeerConfig (params) { - const { org: orgaIdOrName, format } = params.options; - const [networkGroupIdOrLabel, peerId] = params.args; - - const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName); - const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); + const ngData = { + id: ng.id, + label: ng.label, + description: ng.description, + network: `${ng.networkIp}`, + 'members/peers': `${Object.keys(ng.members).length}/${Object.keys(ng.peers).length}`, + }; - Logger.info(`Getting external peer config ${Formatter.formatString(peerId)} from Network Group ${Formatter.formatString(networkGroupId)}`); - const result = await ngApi.getNetworkGroupWireGuardConfiguration({ ownerId, networkGroupId, peerId }).then(sendToApi); - Logger.debug(`Received from API: ${JSON.stringify(result, null, 2)}`); + console.table(ngData); + Logger.println(); - const peerToPrint = result.peers.find((peer) => peer.peer_id === peerId); - result.configuration = Buffer.from(result.configuration, 'base64').toString().replace(/\n+/g, '\n'); + const members = Object.entries(ng.members).map(([id, member]) => ({ domainName: member.domainName })); + if (members.length) { + Logger.println('Members:'); + console.table(members); + } - switch (format) { - case 'json': { - Logger.println(JSON.stringify(result, null, 2)); - break; - } - case 'human': - default: { - Logger.println(`Peer ${Formatter.formatString(peerId)}:`); - Logger.println(` - ${peerToPrint.peer_id} (${peerToPrint.peer_ip})`); - Logger.println(` - ${peerToPrint.peer_hostname}`); - Logger.println(); - Logger.println(`Configuration: ${result.configuration}`); + const peers = Object.entries(ng.peers).map(([id, peer]) => ({ + parent: peer.parentMember, + id: peer.id, + label: peer.label, + IP: peer.endpoint.ngTerm?.host, + publicKey: peer.publicKey, + })); + if (peers.length) { + Logger.println('Peers:'); + console.table(peers); + } } } } diff --git a/src/models/format-string.js b/src/models/format-string.js deleted file mode 100644 index 128afec..0000000 --- a/src/models/format-string.js +++ /dev/null @@ -1,32 +0,0 @@ -import colors from 'colors/safe.js'; - -export function formatId (id) { - return colors.dim(id); -} - -export function formatString (str, decorated = true) { - const string = decorated ? `'${str}'` : str; - return colors.green(string); -} - -export function formatNumber (number) { - return colors.yellow(number); -} - -export function formatIp (ip) { - return colors.cyan(ip); -} - -export function formatUrl (url, decorated = true) { - const string = decorated ? `<${url}>` : url; - return colors.cyan(string); -} - -export function formatCommand (command, decorated = true) { - const string = decorated ? `\`${command}\`` : command; - return colors.magenta(string); -} - -export function formatCode (code, decorated = true) { - return formatCommand(code, decorated); -} diff --git a/src/models/ng-peers.js b/src/models/ng-peers.js new file mode 100644 index 0000000..158a2f8 --- /dev/null +++ b/src/models/ng-peers.js @@ -0,0 +1,50 @@ +import * as autocomplete from 'cliparse'; +import * as Organisation from './organisation.js'; +import * as User from './user.js'; +import * as NG from './ng.js'; +import * as ngApi from '@clevercloud/client/cjs/api/v4/network-group.js'; + +import { sendToApi } from './send-to-api.js'; +import { Logger } from '../logger.js'; +import { v4 as uuidv4 } from 'uuid'; +import colors from 'colors/safe.js'; + +export const TIMEOUT = 5000; +export const INTERVAL = 500; +export const DOMAIN = 'ng.clever-cloud.com'; + +export async function list(orgaIdOrName, networkGroupId) { + const ownerId = (orgaIdOrName != null) + ? await Organisation.getId(orgaIdOrName) + : await User.getCurrentId(); + + Logger.info(`Listing peers from Network Group ${networkGroupId}`); + + const peers = await ngApi.listNetworkGroupPeers({ ownerId, networkGroupId }).then(sendToApi); + + Logger.debug(`Received from API: ${JSON.stringify(peers, null, 2)}`); + + return peers; +} + +export async function get (orgaIdOrName, networkGroupId, peerId) { + + const ownerId = (orgaIdOrName != null) + ? await Organisation.getId(orgaIdOrName) + : await User.getCurrentId(); + + if (!networkGroupId) { + networkGroupId = await NG.getNgIdFromMemberOrPeer(peerId, orgaIdOrName); + } + + if (networkGroupId && networkGroupId.length > 1) { + Logger.error(`Ambiguous Network Group ID: ${networkGroupId}`); + process.exit(1); + } + + Logger.info(`Getting details for peer ${peerId} in Network Group ${networkGroupId}`); + const peer = await ngApi.getNetworkGroupPeer({ ownerId, networkGroupId, peerId }).then(sendToApi); + Logger.debug(`Received from API: ${JSON.stringify(peer, null, 2)}`); + + return peer; +} \ No newline at end of file diff --git a/src/models/ng.js b/src/models/ng.js index bcb95b0..96890ff 100644 --- a/src/models/ng.js +++ b/src/models/ng.js @@ -3,23 +3,18 @@ import * as Organisation from '../models/organisation.js'; import * as User from '../models/user.js'; import * as AppConfig from '../models/app_configuration.js'; import * as ngApi from '@clevercloud/client/cjs/api/v4/network-group.js'; + import { sendToApi } from './send-to-api.js'; +import { Logger } from '../logger.js'; +import { v4 as uuidv4 } from 'uuid'; +import colors from 'colors/safe.js'; -export async function getOwnerId (orgaIdOrName, alias) { - if (orgaIdOrName == null) { - try { - return (await AppConfig.getAppDetails({ alias })).ownerId; - } - catch (error) { - return (await User.getCurrentId()); - } - } - else { - return (await Organisation.getId(orgaIdOrName)); - } -} +export const TIMEOUT = 5000; +export const INTERVAL = 500; +export const DOMAIN = 'ng.clever-cloud.com'; + +export async function getId (orgaIdOrName, ngIdOrLabel) { -export async function getId (ownerId, ngIdOrLabel) { if (ngIdOrLabel == null) { return null; } @@ -28,12 +23,16 @@ export async function getId (ownerId, ngIdOrLabel) { return ngIdOrLabel.ngId; } - return getByLabel(ownerId, ngIdOrLabel.ngLabel) + return getByLabel(orgaIdOrName, ngIdOrLabel.ngLabel) .then((ng) => ng.id); } -async function getByLabel (ownerId, label) { +async function getByLabel (orgaIdOrName, label) { + const ownerId = (orgaIdOrName != null) + ? await Organisation.getId(orgaIdOrName) + : await User.getCurrentId(); + const networkGroups = await ngApi.listNetworkGroups({ ownerId }).then(sendToApi); const filteredNgs = networkGroups.filter((ng) => ng.label === label); @@ -47,10 +46,225 @@ async function getByLabel (ownerId, label) { return filteredNgs[0]; } -export function listAvailablePeerRoles () { +export function listAvailablePeerRoles() { return autocomplete.words(['client', 'server']); } -export function listAvailableMemberTypes () { +export function listAvailableMemberTypes() { return autocomplete.words(['application', 'addon', 'external']); } + +/** + * Get the Network Group ID from the label or ID + * @param {string} ngIdOrLabel The Network Group ID or label + * @param {string} orgaIdOrName The owner ID or name + * @returns {Promise} The Network Group ID + */ +export async function getNgId(ngIdOrLabel, orgaIdOrName) { + const ngs = await getNGs(orgaIdOrName); + const ng = ngs.find((ng) => ng.id === ngIdOrLabel || ng.label === ngIdOrLabel); + + Logger.debug(`Found Network Group (org: ${orgaIdOrName}): ${JSON.stringify(ng, null, 2)}`); + return ng ? ng.id : null; +} + +/** + * Get the Network Group member ID or peer ID from a label or ID + * @param {string} memberOrPeer The Network Group member ID/label or peer ID/label + * @param {string} orgaIdOrName The owner ID or name + * @returns {Promise>} The found Network Group member IDs or peer IDs + */ +export async function getIdFromMemberOrPeer (memberOrPeer, orgaIdOrName) { + const ngs = await getNGs(orgaIdOrName); + + const results = []; + ngs.forEach((ng) => [...ng.members, ...ng.peers] + .filter((item) => item.id === memberOrPeer || item.label === memberOrPeer) + .forEach((item) => !results.includes(item.id) && results.push(item.id))); + + Logger.debug(`Found ${JSON.stringify(results, null, 2)}`); + + return results; +} + +/** + * Get the Network Group ID from a member/peer label or ID + * @param {string} memberOrPeer The Network Group member ID/label or peer ID/label + * @param {string} orgaIdOrName The owner ID or name + * @returns {Promise>} The found Network Group member IDs or peer IDs + */ +export async function getNgIdFromMemberOrPeer (memberOrPeer, orgaIdOrName) { + const ngs = await getNGs(orgaIdOrName); + + const results = []; + ngs.forEach((ng) => [...ng.members, ...ng.peers] + .filter((item) => item.id === memberOrPeer || item.label === memberOrPeer) + .forEach((item) => !results.includes(ng.id) && results.push(ng.id))); + + Logger.debug(`Found ${JSON.stringify(results, null, 2)}`); + + return results; +} + +/** + * Get Network Groups from an owner + * @param {string} orgaIdOrName The owner ID or name + * @returns {Promise>} The list of Network Groups + */ +export async function getNGs (orgaIdOrName) { + const ownerId = (orgaIdOrName != null) + ? await Organisation.getId(orgaIdOrName) + : await User.getCurrentId(); + + Logger.info(`Listing Network Groups from owner ${colors.green(ownerId)}`); + + const result = await ngApi.listNetworkGroups({ ownerId }).then(sendToApi); + Logger.debug(`Received from API:\n${JSON.stringify(result, null, 2)}`); + + return result; +} + +/** + * Get a Network Group + * @param {string} ngIdOrLabel The Network Group ID or label + * @param {string} orgaIdOrName The owner ID or name + * @returns {Promise} The Network Group + */ +export async function get (ngIdOrLabel, orgaIdOrName) { + + const ownerId = (orgaIdOrName != null) + ? await Organisation.getId(orgaIdOrName) + : await User.getCurrentId(); + + const networkGroupId = await getId(ownerId, ngIdOrLabel); + + Logger.info(`Get Next Generation Network Group ${colors.green(networkGroupId)} from owner ${colors.green(ownerId)}`); + const result = await ngApi.getNetworkGroup({ ownerId, networkGroupId }).then(sendToApi); + Logger.debug(`Received from API:\n${JSON.stringify(result, null, 2)}`); + + return result; +} + +export async function destroy (ngIdOrLabel, orgaIdOrName) { + const ownerId = (orgaIdOrName != null) + ? await Organisation.getId(orgaIdOrName) + : await User.getCurrentId(); + + const networkGroupId = await getId(ownerId, ngIdOrLabel); + + Logger.info(`Deleting Network Group ${colors.green(networkGroupId)} from owner ${colors.green(ownerId)}`); + await ngApi.deleteNetworkGroup({ ownerId, networkGroupId }).then(sendToApi); + + return { ownerId, id: networkGroupId }; +} + +export async function create (orgaIdOrName, label, description, tags, membersIds) { + const id = `ng_${uuidv4()}`; + const ownerId = (orgaIdOrName != null) + ? await Organisation.getId(orgaIdOrName) + : await User.getCurrentId(); + + 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 ngApi.createNetworkGroup({ ownerId }, body).then(sendToApi); + + return { id, ownerId }; +} + +/** + * Construct members from members_ids + * @param {Array} members_ids + * @returns {Array} Array of members with id, domainName and type + */ +function constructMembers (ngId, membersIds) { + return membersIds.map((id) => { + + const domainName = `${id}.m.${ngId}.${DOMAIN}`; + const prefixToType = { + app_: 'application', + addon_: 'addon', + external_: 'external', + }; + + return { + id, + domainName, + // Get type from prefix match in id (app_*, addon_*, external_*) or default to 'application' + type: prefixToType[Object.keys(prefixToType).find(p => id.startsWith(p))] || 'application', + }; + }); +} + +/** + * Poll Network Groups to check has been created and if its members are present + * @param {string} ownerId + * @param {string} ngId + * @param {Array} membersIds + */ +export function pollNewNetworkGroup (ownerId, ngId, membersIds) { + Logger.info(`Polling Network Groups from owner ${ownerId}`); + const poll = setInterval(async () => { + const ngList = await ngApi.listNetworkGroups({ ownerId }).then(sendToApi); + const ng = ngList.find((ng) => ng.id === ngId); + Logger.debug(`Received from API during polling: ${JSON.stringify(ng, null, 2)}`); + + if (!ng) { + return; + } + + Logger.info(`Found NG: ${JSON.stringify(ng, null, 2)}`); + + if (membersIds && membersIds.length) { + Logger.info(`Looking for members IDs: ${membersIds.join(', ')}`); + const members = ng.members.filter((member) => membersIds.includes(member.id)); + const allMembersPresent = members.length === membersIds.length; + + if (!allMembersPresent) { + Logger.debug(`Members not found: ${members.map((member) => member.id).join(', ')}`); + return; + } + } + clearInterval(poll); + clearTimeout(timer); + + // Show a message, with members ID if any + const membersIdsMessage = membersIds ? ` with members:\n ▶ ${membersIds.join('\n ▶ ')}` : ''; + Logger.println(`✅ Network Group ${colors.green(ngId)} has been created successfully${membersIdsMessage}`); + }, INTERVAL); + + const timer = setTimeout(() => { + clearInterval(poll); + Logger.error(`Timeout while checking Network Group ${ngId}`); + }, TIMEOUT); +} + +/** + * Poll Network Groups to check has been deleted + * @param {string} ownerId + * @param {string} ngId + */ +export function pollDeletedNetworkGroup (ownerId, ngId) { + Logger.info(`Polling Network Groups from owner ${ownerId}`); + const poll = setInterval(async () => { + const ngList = await ngApi.listNetworkGroups({ ownerId }).then(sendToApi); + const ng = ngList.find((ng) => ng.id === ngId); + Logger.debug(`Received from API during polling: ${JSON.stringify(ng, null, 2)}`); + + if (ng) { + return; + } + clearInterval(poll); + clearTimeout(timer); + + Logger.println(`✅ Network Group ${colors.green(ngId)} has been deleted successfully`); + }, INTERVAL); + + const timer = setTimeout(() => { + clearInterval(poll); + Logger.error(`Timeout while checking Network Group ${ngId}`); + }, TIMEOUT); +}