clever-tools 4.0.2 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,15 +39,24 @@ function getFavouriteDomain({ ownerId, appId }) {
39
39
  }
40
40
 
41
41
  export async function list(params) {
42
- const { alias, app: appIdOrName } = params.options;
42
+ const { alias, app: appIdOrName, format } = params.options;
43
43
  const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
44
44
 
45
45
  const app = await getApp({ id: ownerId, appId }).then(sendToApi);
46
46
  const favouriteDomain = await getFavouriteDomain({ ownerId, appId });
47
- return app.vhosts.forEach(({ fqdn }) => {
48
- const prefix = fqdn === favouriteDomain ? '* ' : ' ';
49
- Logger.println(prefix + fqdn);
50
- });
47
+
48
+ const domains = app.vhosts.map((vhost) => getDomainObject(vhost.fqdn, favouriteDomain));
49
+
50
+ switch (format) {
51
+ case 'json':
52
+ Logger.printJson(domains);
53
+ break;
54
+ default:
55
+ domains.forEach((domain) => {
56
+ Logger.println(`${domain.isFavourite ? '* ' : ' '}${domain.domainWithPathPrefix}`);
57
+ });
58
+ break;
59
+ }
51
60
  }
52
61
 
53
62
  export async function add(params) {
@@ -61,16 +70,23 @@ export async function add(params) {
61
70
  }
62
71
 
63
72
  export async function getFavourite(params) {
64
- const { alias, app: appIdOrName } = params.options;
73
+ const { alias, app: appIdOrName, format } = params.options;
65
74
  const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
66
75
 
67
76
  const favouriteDomain = await getFavouriteDomain({ ownerId, appId });
68
77
 
69
- if (favouriteDomain == null) {
70
- return Logger.println('No favourite domain set');
78
+ switch (format) {
79
+ case 'json':
80
+ const domain = getDomainObject(favouriteDomain, favouriteDomain);
81
+ Logger.printJson(domain);
82
+ break;
83
+ default:
84
+ if (favouriteDomain == null) {
85
+ return Logger.println('No favourite domain set');
86
+ }
87
+ Logger.println(favouriteDomain);
88
+ break;
71
89
  }
72
-
73
- return Logger.println(favouriteDomain);
74
90
  }
75
91
 
76
92
  export async function setFavourite(params) {
@@ -258,6 +274,21 @@ export async function overview(params) {
258
274
  }
259
275
  }
260
276
 
277
+ function getDomainObject(domainWithPathPrefix, favouriteDomain) {
278
+ const parsed = parseDomain(domainWithPathPrefix, { validateHostname: false });
279
+ return {
280
+ domainWithPathPrefix,
281
+ domain: parsed.domain,
282
+ domainWithoutSuffix: parsed.domainWithoutSuffix,
283
+ hostname: parsed.hostname,
284
+ publicSuffix: parsed.publicSuffix,
285
+ subdomain: parsed.subdomain,
286
+ isApex: parsed.subdomain === '',
287
+ pathPrefix: new URL('https://' + domainWithPathPrefix).pathname,
288
+ isFavourite: domainWithPathPrefix === favouriteDomain,
289
+ };
290
+ }
291
+
261
292
  /** @param {DomainDiag & { resolvedDnsConfig: ResolveDnsResult }} domainDiag */
262
293
  function reportDomainDiagnostics({ hostname, pathPrefix, resolvedDnsConfig, diagDetails, diagSummary }) {
263
294
  const validDiags = diagDetails.filter((diag) => diag.code === 'valid-a');
@@ -274,20 +305,20 @@ function reportDomainDiagnostics({ hostname, pathPrefix, resolvedDnsConfig, diag
274
305
 
275
306
  switch (diagSummary) {
276
307
  case 'managed':
277
- printlnWithIndent(styleText('green', '✔ Managed by Clever Cloud'), 2);
278
- printlnWithIndent('ⓘ cleverapps.io domains should only be used for testing purposes', 2);
308
+ Logger.printlnWithIndent(styleText('green', '✔ Managed by Clever Cloud'), 2);
309
+ Logger.printlnWithIndent('ⓘ cleverapps.io domains should only be used for testing purposes', 2);
279
310
  break;
280
311
  case 'no-config':
281
- printlnWithIndent(styleText('red', '✘ No DNS configuration found'), 2);
312
+ Logger.printlnWithIndent(styleText('red', '✘ No DNS configuration found'), 2);
282
313
  break;
283
314
  case 'valid':
284
- printlnWithIndent(styleText('green', '✔ Your configuration is valid'), 2);
315
+ Logger.printlnWithIndent(styleText('green', '✔ Your configuration is valid'), 2);
285
316
  break;
286
317
  case 'invalid':
287
- printlnWithIndent(styleText('red', '✘ Something is wrong with your configuration'), 2);
318
+ Logger.printlnWithIndent(styleText('red', '✘ Something is wrong with your configuration'), 2);
288
319
  break;
289
320
  case 'incomplete':
290
- printlnWithIndent(styleText('yellow', '⚠ Your configuration is incomplete'), 2);
321
+ Logger.printlnWithIndent(styleText('yellow', '⚠ Your configuration is incomplete'), 2);
291
322
  break;
292
323
  }
293
324
 
@@ -296,15 +327,15 @@ function reportDomainDiagnostics({ hostname, pathPrefix, resolvedDnsConfig, diag
296
327
  Logger.println('');
297
328
  validDiags.forEach((diag) => {
298
329
  const source = hasCnameRecord ? `(from CNAME ${resolvedDnsConfig.cnameRecords[0]}.)` : '';
299
- printlnWithIndent(`${diag.record.value} ${styleText('green', '✔ A Record OK')} ${source}`, 2);
330
+ Logger.printlnWithIndent(`${diag.record.value} ${styleText('green', '✔ A Record OK')} ${source}`, 2);
300
331
  });
301
332
  }
302
333
 
303
334
  // Replace A with CNAME
304
335
  if (diagSummary === 'valid' && suggestedCname != null) {
305
336
  Logger.println('');
306
- printlnWithIndent('ⓘ You can replace your A records with this CNAME:', 2);
307
- printlnWithIndent(suggestedCname.record.value, 6);
337
+ Logger.printlnWithIndent('ⓘ You can replace your A records with this CNAME:', 2);
338
+ Logger.printlnWithIndent(suggestedCname.record.value, 6);
308
339
  }
309
340
 
310
341
  // Replace A Records with CNAME
@@ -312,51 +343,51 @@ function reportDomainDiagnostics({ hostname, pathPrefix, resolvedDnsConfig, diag
312
343
  const cnameToUse = suggestedCname != null ? suggestedCname.record.value : missingCname.record.value;
313
344
 
314
345
  Logger.println('');
315
- printlnWithIndent('⇄ Replace your A records with this CNAME:', 2);
316
- printlnWithIndent(cnameToUse, 6);
346
+ Logger.printlnWithIndent('⇄ Replace your A records with this CNAME:', 2);
347
+ Logger.printlnWithIndent(cnameToUse, 6);
317
348
  Logger.println('');
318
- printlnWithIndent('or:', 2);
349
+ Logger.printlnWithIndent('or:', 2);
319
350
  }
320
351
 
321
352
  // Replace unknown CNAME with missing CNAME
322
353
  if (diagSummary === 'invalid' && missingCname != null && unknownCname != null) {
323
354
  Logger.println('');
324
- printlnWithIndent('➖Remove this CNAME record:', 2);
325
- printlnWithIndent(unknownCname.record.value + '. ', 6);
355
+ Logger.printlnWithIndent('➖Remove this CNAME record:', 2);
356
+ Logger.printlnWithIndent(unknownCname.record.value + '. ', 6);
326
357
  Logger.println('');
327
- printlnWithIndent('➕Add this CNAME record instead:', 2);
328
- printlnWithIndent(missingCname.record.value, 6);
358
+ Logger.printlnWithIndent('➕Add this CNAME record instead:', 2);
359
+ Logger.printlnWithIndent(missingCname.record.value, 6);
329
360
 
330
361
  if (hasARecords) {
331
362
  Logger.println('');
332
- printlnWithIndent('or:', 2);
363
+ Logger.printlnWithIndent('or:', 2);
333
364
  }
334
365
  }
335
366
 
336
367
  // Add CNAME
337
368
  if (diagSummary === 'no-config' && missingCname != null) {
338
369
  Logger.println('');
339
- printlnWithIndent('➕Add this CNAME record:', 2);
340
- printlnWithIndent(missingCname.record.value, 6);
370
+ Logger.printlnWithIndent('➕Add this CNAME record:', 2);
371
+ Logger.printlnWithIndent(missingCname.record.value, 6);
341
372
  }
342
373
 
343
374
  // Remove Unknown records
344
375
  if (unknownDiags.length > 0) {
345
376
  Logger.println('');
346
- printlnWithIndent('➖Remove these A records:', 2);
377
+ Logger.printlnWithIndent('➖Remove these A records:', 2);
347
378
 
348
379
  unknownDiags.forEach((diag) => {
349
380
  const source = hasCnameRecord ? `(from CNAME ${resolvedDnsConfig.cnameRecords[0]}.)` : '';
350
- printlnWithIndent(`${diag.record.value} ${source}`, 6);
381
+ Logger.printlnWithIndent(`${diag.record.value} ${source}`, 6);
351
382
  });
352
383
  }
353
384
 
354
385
  // Add missing A records
355
386
  if (missingDiags.length > 0) {
356
387
  Logger.println('');
357
- printlnWithIndent('➕Add these A records:', 2);
388
+ Logger.printlnWithIndent('➕Add these A records:', 2);
358
389
 
359
- missingDiags.forEach((missingDiag) => printlnWithIndent(missingDiag.record.value, 6));
390
+ missingDiags.forEach((missingDiag) => Logger.printlnWithIndent(missingDiag.record.value, 6));
360
391
  }
361
392
  }
362
393
 
@@ -375,16 +406,6 @@ function getParsedDomains(vhosts) {
375
406
  });
376
407
  }
377
408
 
378
- /**
379
- * Prints a line of text with specified indentation.
380
- *
381
- * @param {string} text - The text to be printed.
382
- * @param {number} indentLevel - The number of spaces to indent the text.
383
- */
384
- function printlnWithIndent(text, indentLevel) {
385
- Logger.println(' '.repeat(indentLevel) + text);
386
- }
387
-
388
409
  function recursiveSort(obj) {
389
410
  if (typeof obj === 'object' && obj.appId != null) {
390
411
  return obj;
@@ -407,15 +428,15 @@ function recursiveSort(obj) {
407
428
 
408
429
  function recursiveDisplay(obj, indentLevel = 0) {
409
430
  if (typeof obj === 'object' && obj.appId != null) {
410
- printlnWithIndent(`${obj.ownerName} | ${obj.appName} (${obj.appVariantSlug})`, indentLevel);
411
- printlnWithIndent(styleText('blue', obj.appConsoleUrl), indentLevel);
431
+ Logger.printlnWithIndent(`${obj.ownerName} | ${obj.appName} (${obj.appVariantSlug})`, indentLevel);
432
+ Logger.printlnWithIndent(styleText('blue', obj.appConsoleUrl), indentLevel);
412
433
  return;
413
434
  }
414
435
 
415
436
  for (const [propertyPath, subObj] of Object.entries(obj)) {
416
437
  if (propertyPath !== '/') {
417
438
  Logger.println('');
418
- printlnWithIndent(styleText('yellow', propertyPath), indentLevel);
439
+ Logger.printlnWithIndent(styleText('yellow', propertyPath), indentLevel);
419
440
  recursiveDisplay(subObj, indentLevel + 2);
420
441
  } else {
421
442
  recursiveDisplay(subObj, indentLevel);
@@ -1,13 +1,12 @@
1
1
  import {
2
2
  todo_addEmailAddress as addEmailAddress,
3
- todo_getEmailAddresses as getEmailAddresses,
4
3
  todo_removeEmailAddress as removeEmailAddress,
5
4
  } from '@clevercloud/client/esm/api/v2/user.js';
6
5
  import { confirm } from '../lib/prompts.js';
7
6
  import { styleText } from '../lib/style-text.js';
8
7
  import { Logger } from '../logger.js';
8
+ import { getUserEmailAddresses } from '../models/emails.js';
9
9
  import { sendToApi } from '../models/send-to-api.js';
10
- import * as User from '../models/user.js';
11
10
  import { openBrowser } from '../models/utils.js';
12
11
 
13
12
  /**
@@ -155,16 +154,3 @@ export async function removeAllSecondary(params) {
155
154
  export function openConsole() {
156
155
  return openBrowser('/users/me/emails', 'Opening the email addresses management page of the Console in your browser');
157
156
  }
158
-
159
- /**
160
- * Get the primary and secondary email addresses of the current user
161
- * @returns {Promise<{ primary: string, secondary: string[] }>} The primary and secondary email addresses of the current user
162
- */
163
- async function getUserEmailAddresses() {
164
- const currentUser = await User.getCurrent();
165
- const secondaryAddresses = await getEmailAddresses().then(sendToApi);
166
- return {
167
- primary: currentUser.email,
168
- secondary: secondaryAddresses.sort(),
169
- };
170
- }
@@ -6,15 +6,15 @@ export async function link(params) {
6
6
  const [app] = params.args;
7
7
  const { org: orgaIdOrName, alias } = params.options;
8
8
 
9
+ let appConfigEntry;
9
10
  if (app.app_id != null && orgaIdOrName != null) {
10
11
  Logger.warn("You've specified a unique application ID, organisation option will be ignored");
11
- await Application.linkRepo(app, null, alias);
12
+ appConfigEntry = await Application.linkRepo(app, null, alias);
12
13
  } else {
13
- await Application.linkRepo(app, orgaIdOrName, alias);
14
+ appConfigEntry = await Application.linkRepo(app, orgaIdOrName, alias);
14
15
  }
15
16
 
16
- const linkedMessage = alias ? ` to local alias ${styleText('green', alias)}` : '';
17
17
  Logger.printSuccess(
18
- `Application ${styleText('green', app.app_name || app.app_id)} has been successfully linked${linkedMessage}!`,
18
+ `Application ${styleText('green', appConfigEntry.app_id)} has been successfully linked to local alias ${styleText('green', appConfigEntry.alias)}!`,
19
19
  );
20
20
  }
@@ -47,7 +47,7 @@ export async function deleteNg(params) {
47
47
  * @param {Object} params
48
48
  * @param {Object} params.args[0] External peer ID or label
49
49
  * @param {Object} params.args[1] Network Group ID or label
50
- * @param {string} params.args[2] Wireguard public key
50
+ * @param {string} params.args[2] WireGuard public key
51
51
  * @param {Object} params.options.org Organisation ID or name
52
52
  */
53
53
  export async function createExternalPeer(params) {
@@ -1,14 +1,11 @@
1
- import {
2
- todo_addSshKey as addSshKey,
3
- todo_getSshKeys as getSshKeys,
4
- todo_removeSshKey as removeSshKey,
5
- } from '@clevercloud/client/esm/api/v2/user.js';
1
+ import { todo_addSshKey as addSshKey, todo_removeSshKey as removeSshKey } from '@clevercloud/client/esm/api/v2/user.js';
6
2
  import dedent from 'dedent';
7
3
  import fs from 'node:fs';
8
4
  import { confirm } from '../lib/prompts.js';
9
5
  import { styleText } from '../lib/style-text.js';
10
6
  import { Logger } from '../logger.js';
11
7
  import { sendToApi } from '../models/send-to-api.js';
8
+ import { getUserSshKeys } from '../models/ssh-keys.js';
12
9
  import { openBrowser } from '../models/utils.js';
13
10
 
14
11
  /**
@@ -145,11 +142,3 @@ export async function removeAll(params) {
145
142
  export function openConsole() {
146
143
  return openBrowser('/users/me/ssh-keys', 'Opening the SSH keys management page of the Console in your browser');
147
144
  }
148
-
149
- /**
150
- * @return {Promise<Array<{ name: string, key: string, fingerprint: string }>>}
151
- */
152
- async function getUserSshKeys() {
153
- const rawKeys = await getSshKeys().then(sendToApi);
154
- return rawKeys.sort((a, b) => a.name.localeCompare(b.name));
155
- }
@@ -27,6 +27,7 @@ export async function status(params) {
27
27
  : styleText(['bold', 'red'], 'stopped');
28
28
 
29
29
  Logger.println(`${status.name}: ${statusMessage}`);
30
+ Logger.println(`Type: ${status.type.name}`);
30
31
  Logger.println(`Executed as: ${styleText('bold', status.lifetime)}`);
31
32
  if (status.deploymentInProgress) {
32
33
  Logger.println(
@@ -67,6 +68,10 @@ function computeStatus(instances, app) {
67
68
  const status = {
68
69
  id: app.id,
69
70
  name: app.name,
71
+ type: {
72
+ name: app.instance.variant.name,
73
+ slug: app.instance.variant.slug,
74
+ },
70
75
  lifetime: app.instance.lifetime,
71
76
  status: isUp ? 'running' : 'stopped',
72
77
  commit: upCommit,
@@ -1,6 +1,4 @@
1
1
  import { addTcpRedir, getTcpRedirs, removeTcpRedir } from '@clevercloud/client/esm/api/v2/application.js';
2
- import { confirm } from '../lib/prompts.js';
3
- import { styleText } from '../lib/style-text.js';
4
2
  import { Logger } from '../logger.js';
5
3
  import * as Application from '../models/application.js';
6
4
  import * as Namespaces from '../models/namespaces.js';
@@ -64,37 +62,10 @@ export async function list(params) {
64
62
  }
65
63
  }
66
64
 
67
- async function acceptPayment(result, skipConfirmation) {
68
- if (!skipConfirmation) {
69
- result.lines.forEach(({ description, VAT, price }) =>
70
- Logger.println(`${description}\tVAT: ${VAT}%\tPrice: ${price}€`),
71
- );
72
- Logger.println(`Total (without taxes): ${result.totalHT}€`);
73
- Logger.println(styleText('bold', `Total (with taxes): ${result.totalTTC}€`));
74
-
75
- await confirm(
76
- `You're about to pay ${result.totalTTC}€, confirm?`,
77
- 'No confirmation, aborting TCP redirection creation',
78
- );
79
- }
80
- }
81
-
82
65
  export async function add(params) {
83
- const { alias, app: appIdOrName, namespace, yes: skipConfirmation } = params.options;
66
+ const { alias, app: appIdOrName, namespace } = params.options;
84
67
  const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
85
-
86
- const { port } = await addTcpRedir({ id: ownerId, appId }, { namespace })
87
- .then(sendToApi)
88
- .catch((error) => {
89
- if (error.status === 402) {
90
- return acceptPayment(error.response.body, skipConfirmation).then(() => {
91
- return addTcpRedir({ id: ownerId, appId, payment: 'accepted' }, { namespace }).then(sendToApi);
92
- });
93
- } else {
94
- throw error;
95
- }
96
- });
97
-
68
+ const { port } = await addTcpRedir({ id: ownerId, appId }, { namespace }).then(sendToApi);
98
69
  Logger.println('Successfully added tcp redirection on port: ' + port);
99
70
  }
100
71
 
@@ -1,5 +1,6 @@
1
1
  import dedent from 'dedent';
2
2
  import { createApiToken, deleteApiToken, listApiTokens } from '../clever-client/auth-bridge.js';
3
+ import { formatDate } from '../lib/format-date.js';
3
4
  import { promptSecret } from '../lib/prompts.js';
4
5
  import { styleText } from '../lib/style-text.js';
5
6
  import { Logger } from '../logger.js';
@@ -141,7 +142,3 @@ export async function revoke(params) {
141
142
 
142
143
  Logger.println(styleText('green', '✔'), 'API token successfully revoked!');
143
144
  }
144
-
145
- function formatDate(dateInput) {
146
- return new Date(dateInput).toISOString().substring(0, 16).replace('T', ' ');
147
- }
@@ -3,7 +3,7 @@ import { conf } from './models/configuration.js';
3
3
 
4
4
  export const EXPERIMENTAL_FEATURES = {
5
5
  kv: {
6
- status: 'alpha',
6
+ status: 'beta',
7
7
  description:
8
8
  'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
9
9
  instructions: dedent`
@@ -20,20 +20,20 @@ export const EXPERIMENTAL_FEATURES = {
20
20
  },
21
21
  ng: {
22
22
  status: 'beta',
23
- description: 'Manage Network Groups to manage applications, add-ons, external peers through a Wireguard network',
23
+ description: 'Manage Network Groups to manage applications, add-ons, external peers through a WireGuard network',
24
24
  instructions: dedent`
25
25
  - Create a Network Group:
26
26
  clever ng create myNG
27
27
  - Create a Network Group with members (application, database add-on):
28
- clever ng create myNG --link app_xxx,addon_xxx
28
+ clever ng create myNG --link app_xxx,postgresql_xxx
29
29
  - List Network Groups:
30
30
  clever ng
31
31
  - Delete a Network Group:
32
32
  clever ng delete myNG
33
33
  - (Un)Link an application or a database add-on to an existing Network Group:
34
34
  clever ng link app_xxx myNG
35
- clever ng unlink addon_xxx myNG
36
- - Get the Wireguard configuration of a peer:
35
+ clever ng unlink postgresql_xxx myNG
36
+ - Get the WireGuard configuration of a peer:
37
37
  clever ng get-config peerIdOrLabel myNG
38
38
  - Get details about a Network Group, a member or a peer:
39
39
  clever ng get myNg
@@ -0,0 +1,3 @@
1
+ export function formatDate(dateInput) {
2
+ return new Date(dateInput).toISOString().substring(0, 16).replace('T', ' ');
3
+ }
package/src/logger.js CHANGED
@@ -58,6 +58,16 @@ export const Logger = _(['debug', 'info', 'warn', 'error'])
58
58
  // No decoration for Logger.println
59
59
  Logger.println = console.log;
60
60
 
61
+ /**
62
+ * Prints a line of text with specified indentation.
63
+ *
64
+ * @param {string} text - The text to be printed.
65
+ * @param {number} indentLevel - The number of spaces to indent the text.
66
+ */
67
+ Logger.printlnWithIndent = (text, indentLevel) => {
68
+ Logger.println(' '.repeat(indentLevel) + text);
69
+ };
70
+
61
71
  // Logger for success with a green check before the message
62
72
  Logger.printSuccess = (message) => console.log(`${styleText(['bold', 'green'], '✓')} ${message}`);
63
73
 
@@ -2,6 +2,7 @@ import _ from 'lodash';
2
2
  import { promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { slugify } from '../lib/slugify.js';
5
+ import { styleText } from '../lib/style-text.js';
5
6
  import { Logger } from '../logger.js';
6
7
  import { conf } from './configuration.js';
7
8
  import * as User from './user.js';
@@ -39,14 +40,17 @@ export async function addLinkedApplication(appData, alias, ignoreParentConfig) {
39
40
  };
40
41
 
41
42
  const isPresent = currentConfig.apps.find((app) => app.app_id === appEntry.app_id) != null;
42
-
43
- // ToDo see what to do when there is a conflict between an existing entry
44
- // and the entry we want to add (same app_id, different other values)
45
- if (!isPresent) {
46
- currentConfig.apps.push(appEntry);
43
+ if (isPresent) {
44
+ throw new Error(
45
+ `Application ${styleText('red', appEntry.app_id)} is already linked with alias ${styleText('red', appEntry.alias)}`,
46
+ );
47
47
  }
48
48
 
49
- return persistConfig(currentConfig);
49
+ currentConfig.apps.push(appEntry);
50
+
51
+ return persistConfig(currentConfig).then(() => {
52
+ return appEntry;
53
+ });
50
54
  }
51
55
 
52
56
  export async function removeLinkedApplication({ appId, alias }) {
@@ -0,0 +1,16 @@
1
+ import { todo_getEmailAddresses as getEmailAddresses } from '@clevercloud/client/esm/api/v2/user.js';
2
+ import { sendToApi } from './send-to-api.js';
3
+ import * as User from './user.js';
4
+
5
+ /**
6
+ * Get the primary and secondary email addresses of the current user
7
+ * @returns {Promise<{ primary: string, secondary: string[] }>} The primary and secondary email addresses of the current user
8
+ */
9
+ export async function getUserEmailAddresses() {
10
+ const currentUser = await User.getCurrent();
11
+ const secondaryAddresses = await getEmailAddresses().then(sendToApi);
12
+ return {
13
+ primary: currentUser.email,
14
+ secondary: secondaryAddresses.sort(),
15
+ };
16
+ }
@@ -17,7 +17,8 @@ const THROTTLE_PER_IN_MILLISECONDS = 100;
17
17
 
18
18
  const retryConfiguration = {
19
19
  enabled: true,
20
- maxRetryCount: 6,
20
+ initRetryTimeout: 3000,
21
+ maxRetryCount: 10,
21
22
  };
22
23
 
23
24
  export async function displayLogs(params) {
@@ -64,7 +65,7 @@ export async function displayLogs(params) {
64
65
  jsonArray.push(log);
65
66
  return;
66
67
  case 'json-stream':
67
- Logger.printJson(log);
68
+ Logger.println(JSON.stringify(log));
68
69
  return;
69
70
  case 'human':
70
71
  default:
@@ -1,10 +1,10 @@
1
- import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
2
1
  import * as networkGroupApi from '@clevercloud/client/esm/api/v4/network-group.js';
3
2
  import crypto from 'node:crypto';
4
3
  import { setTimeout } from 'node:timers/promises';
5
4
  import { styleText } from '../lib/style-text.js';
6
5
  import { Logger } from '../logger.js';
7
6
  import * as networkGroup from './ng.js';
7
+ import { NG_MEMBER_PREFIXES } from './ng.js';
8
8
  import { sendToApi } from './send-to-api.js';
9
9
 
10
10
  /**
@@ -128,14 +128,16 @@ export async function deleteExternalPeerWithParent(ngIdOrLabel, peerIdOrLabel, o
128
128
 
129
129
  /**
130
130
  * Link a Member to a Network Group
131
- * @param {object} ngIdOrLabel The Network group ID or Label
131
+ * @param {object} ngIdOrLabel The Network Group ID or Label
132
132
  * @param {string} memberId ID of the Member to link
133
133
  * @param {object} org Organisation ID or name
134
134
  * @param {string} label Label of the Member
135
135
  */
136
136
  export async function linkMember(ngIdOrLabel, memberId, org, label) {
137
137
  if (!memberId) {
138
- throw new Error('A valid member ID is required (addon_xxx, app_xxx, external_xxx)');
138
+ throw new Error(
139
+ 'A valid member ID is required (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.)',
140
+ );
139
141
  }
140
142
 
141
143
  const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
@@ -144,7 +146,7 @@ export async function linkMember(ngIdOrLabel, memberId, org, label) {
144
146
  throw new Error(`Network Group ${styleText('red', ngIdOrLabel.ngId || ngIdOrLabel.ngResourceLabel)} not found`);
145
147
  }
146
148
 
147
- await checkMembersToLink([memberId], ng.ownerId);
149
+ checkMembersToLink([memberId]);
148
150
 
149
151
  const alreadyMember = ng.members.find((m) => m.id === memberId);
150
152
  if (alreadyMember) {
@@ -186,7 +188,9 @@ export async function linkMember(ngIdOrLabel, memberId, org, label) {
186
188
  */
187
189
  export async function unlinkMember(ngIdOrLabel, memberId, org) {
188
190
  if (!memberId) {
189
- throw new Error('A valid member ID is required (addon_xxx, app_xxx, external_xxx)');
191
+ throw new Error(
192
+ 'A valid member ID is required (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.)',
193
+ );
190
194
  }
191
195
 
192
196
  const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
@@ -215,39 +219,23 @@ export async function unlinkMember(ngIdOrLabel, memberId, org) {
215
219
 
216
220
  /**
217
221
  * Check if members can be linked to a Network Group
218
- * @param {Array<string>} members Members to check
222
+ * @param {Array<string>} memberIds Members to check
219
223
  * @throws {Error} If members can't be linked to a Network Group
220
224
  */
221
- export async function checkMembersToLink(members, ownerId) {
222
- const VALID_ADDON_PROVIDERS = ['es-addon', 'mongodb-addon', 'mysql-addon', 'postgresql-addon', 'redis-addon'];
223
-
224
- const summary = await getSummary().then(sendToApi);
225
-
226
- let data = summary.user;
227
- if (summary.user.id !== ownerId) {
228
- data = summary.organisations.find((o) => o.id === ownerId);
229
- }
230
-
225
+ export function checkMembersToLink(memberIds) {
226
+ const validPrefixes = Object.keys(NG_MEMBER_PREFIXES);
231
227
  const membersNotOK = [];
232
- let source = data.applications;
233
-
234
- for (const memberId of members) {
235
- if (memberId.startsWith('addon_')) {
236
- source = data.addons;
237
- }
238
-
239
- const foundRessource = source.find((r) => r.id === memberId);
240
228
 
241
- if (foundRessource && memberId.startsWith('addon_') && !VALID_ADDON_PROVIDERS.includes(foundRessource.providerId)) {
242
- membersNotOK.push(memberId);
243
- } else if (!foundRessource && !memberId.startsWith('external_')) {
229
+ for (const memberId of memberIds) {
230
+ const hasValidPrefix = validPrefixes.some((prefix) => memberId.startsWith(prefix));
231
+ if (!hasValidPrefix) {
244
232
  membersNotOK.push(memberId);
245
233
  }
246
234
  }
247
235
 
248
236
  if (membersNotOK.length > 0) {
249
237
  throw new Error(
250
- `Member(s) ${styleText('red', membersNotOK.join(', '))} can't be linked to the Network Group, check Organisation ID or name`,
238
+ `Member(s) ${styleText('red', membersNotOK.join(', '))} can't be linked to the Network Group, check member ID format`,
251
239
  );
252
240
  }
253
241
  }