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.
- package/bin/clever.js +25 -74
- package/package.json +2 -2
- package/src/commands/accesslogs.js +1 -1
- package/src/commands/addon.js +119 -146
- package/src/commands/diag.js +33 -4
- package/src/commands/domain.js +67 -46
- package/src/commands/emails.js +1 -15
- package/src/commands/link.js +4 -4
- package/src/commands/ng.js +1 -1
- package/src/commands/ssh-keys.js +2 -13
- package/src/commands/status.js +5 -0
- package/src/commands/tcp-redirs.js +2 -31
- package/src/commands/tokens.js +1 -4
- package/src/experimental-features.js +5 -5
- package/src/lib/format-date.js +3 -0
- package/src/logger.js +10 -0
- package/src/models/app_configuration.js +10 -6
- package/src/models/emails.js +16 -0
- package/src/models/log-v4.js +3 -2
- package/src/models/ng-resources.js +16 -28
- package/src/models/ng.js +14 -15
- package/src/models/ssh-keys.js +10 -0
- package/src/parsers.js +3 -2
package/src/commands/domain.js
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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);
|
package/src/commands/emails.js
CHANGED
|
@@ -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
|
-
}
|
package/src/commands/link.js
CHANGED
|
@@ -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',
|
|
18
|
+
`Application ${styleText('green', appConfigEntry.app_id)} has been successfully linked to local alias ${styleText('green', appConfigEntry.alias)}!`,
|
|
19
19
|
);
|
|
20
20
|
}
|
package/src/commands/ng.js
CHANGED
|
@@ -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]
|
|
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) {
|
package/src/commands/ssh-keys.js
CHANGED
|
@@ -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
|
-
}
|
package/src/commands/status.js
CHANGED
|
@@ -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
|
|
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
|
|
package/src/commands/tokens.js
CHANGED
|
@@ -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: '
|
|
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
|
|
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,
|
|
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
|
|
36
|
-
- Get the
|
|
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
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/models/log-v4.js
CHANGED
|
@@ -17,7 +17,8 @@ const THROTTLE_PER_IN_MILLISECONDS = 100;
|
|
|
17
17
|
|
|
18
18
|
const retryConfiguration = {
|
|
19
19
|
enabled: true,
|
|
20
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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>}
|
|
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
|
|
222
|
-
const
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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
|
}
|