All files / src/models ng-resources.js

26.86% Statements 72/268
100% Branches 1/1
0% Functions 0/6
26.86% Lines 72/268

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x                                                                                                             193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x                                                                                 193x 193x 193x 193x 193x 193x 193x 193x 193x                                                                                   193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x                                                           193x 193x 193x 193x 193x 193x 193x                                 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x                                  
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 { NG_MEMBER_PREFIXES } 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 (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.)',
    );
  }

  const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');

  if (!ng) {
    throw new Error(`Network Group ${styleText('red', ngIdOrLabel.ngId || ngIdOrLabel.ngResourceLabel)} not found`);
  }

  checkMembersToLink([memberId]);

  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 (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.)',
    );
  }

  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>} memberIds Members to check
 * @throws {Error} If members can't be linked to a Network Group
 */
export function checkMembersToLink(memberIds) {
  const validPrefixes = Object.keys(NG_MEMBER_PREFIXES);
  const membersNotOK = [];

  for (const memberId of memberIds) {
    const hasValidPrefix = validPrefixes.some((prefix) => memberId.startsWith(prefix));
    if (!hasValidPrefix) {
      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 member ID format`,
    );
  }
}
 
/**
 * 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;
}