clever-tools 2.9.1 → 2.10.0-beta.2
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/CHANGELOG.md +7 -1
- package/bin/clever.js +248 -3
- package/package.json +6 -5
- package/src/commands/applications.js +24 -15
- package/src/commands/networkgroups/commands.js +7 -0
- package/src/commands/networkgroups/index.js +67 -0
- package/src/commands/networkgroups/members.js +80 -0
- package/src/commands/networkgroups/peers.js +83 -0
- package/src/models/app_configuration.js +38 -0
- package/src/models/format-ng-table.js +115 -0
- package/src/models/format-string.js +44 -0
- package/src/models/networkgroup.js +62 -0
- package/src/models/wireguard-conf.js +95 -0
- package/src/models/wireguard.js +75 -0
- package/src/parsers.js +76 -1
package/CHANGELOG.md
CHANGED
package/bin/clever.js
CHANGED
|
@@ -18,6 +18,7 @@ const updateNotifier = require('update-notifier');
|
|
|
18
18
|
const git = require('../src/models/git.js');
|
|
19
19
|
const Parsers = require('../src/parsers.js');
|
|
20
20
|
const handleCommandPromise = require('../src/command-promise-handler.js');
|
|
21
|
+
const Formatter = require('../src/models/format-string.js');
|
|
21
22
|
|
|
22
23
|
// Exit cleanly if the program we pipe to exits abruptly
|
|
23
24
|
process.stdout.on('error', (error) => {
|
|
@@ -69,6 +70,7 @@ const ApplicationConfiguration = lazyRequire('../src/models/application_configur
|
|
|
69
70
|
const Drain = lazyRequire('../src/models/drain.js');
|
|
70
71
|
const Notification = lazyRequire('../src/models/notification.js');
|
|
71
72
|
const Organisation = lazyRequire('../src/models/organisation.js');
|
|
73
|
+
const NetworkGroup = lazyRequire('../src/models/networkgroup.js');
|
|
72
74
|
|
|
73
75
|
function run () {
|
|
74
76
|
|
|
@@ -113,6 +115,11 @@ function run () {
|
|
|
113
115
|
},
|
|
114
116
|
}),
|
|
115
117
|
configurationValue: cliparse.argument('configuration-value', { description: 'The new value of the configuration' }),
|
|
118
|
+
ngId: cliparse.argument('ng-id', { description: 'The Network Group id' }),
|
|
119
|
+
ngIdOrLabel: cliparse.argument('ng', {
|
|
120
|
+
description: 'Network Group ID or label',
|
|
121
|
+
parser: Parsers.ngIdOrLabel,
|
|
122
|
+
}),
|
|
116
123
|
};
|
|
117
124
|
|
|
118
125
|
// OPTIONS
|
|
@@ -132,7 +139,9 @@ function run () {
|
|
|
132
139
|
aliases: ['f'],
|
|
133
140
|
description: 'Display access logs continuously (ignores before/until, after/since)',
|
|
134
141
|
}),
|
|
135
|
-
importAsJson: cliparse.flag('json', {
|
|
142
|
+
importAsJson: cliparse.flag('json', {
|
|
143
|
+
description: 'Import variables as JSON (an array of { "name": "THE_NAME", "value": "the value" } objects)',
|
|
144
|
+
}),
|
|
136
145
|
addonId: cliparse.option('addon', { metavar: 'addon_id', description: 'Addon ID' }),
|
|
137
146
|
after: cliparse.option('after', {
|
|
138
147
|
metavar: 'after',
|
|
@@ -151,6 +160,10 @@ function run () {
|
|
|
151
160
|
description: 'Short name for the application',
|
|
152
161
|
complete: Application('listAvailableAliases'),
|
|
153
162
|
}),
|
|
163
|
+
naturalName: cliparse.flag('natural-name', {
|
|
164
|
+
aliases: ['n'],
|
|
165
|
+
description: 'Show application names or aliases if possible',
|
|
166
|
+
}),
|
|
154
167
|
before: cliparse.option('before', {
|
|
155
168
|
metavar: 'before',
|
|
156
169
|
aliases: ['until'],
|
|
@@ -270,7 +283,7 @@ function run () {
|
|
|
270
283
|
onlyAliases: cliparse.flag('only-aliases', { description: 'List only application aliases' }),
|
|
271
284
|
onlyApps: cliparse.flag('only-apps', { description: 'Only show app dependencies' }),
|
|
272
285
|
orgaIdOrName: cliparse.option('org', {
|
|
273
|
-
aliases: ['o'],
|
|
286
|
+
aliases: ['o', 'owner'],
|
|
274
287
|
description: 'Organisation ID (or name, if unambiguous)',
|
|
275
288
|
parser: Parsers.orgaIdOrName,
|
|
276
289
|
}),
|
|
@@ -366,6 +379,158 @@ function run () {
|
|
|
366
379
|
aliases: ['y'],
|
|
367
380
|
description: 'Skip confirmation even if the TCP redirection is not free',
|
|
368
381
|
}),
|
|
382
|
+
ngLabel: cliparse.option('label', {
|
|
383
|
+
required: true,
|
|
384
|
+
metavar: 'ng_label',
|
|
385
|
+
description: 'Network Group label, also used for dns context',
|
|
386
|
+
}),
|
|
387
|
+
ngIdOrLabel: cliparse.option('ng', {
|
|
388
|
+
required: true,
|
|
389
|
+
metavar: 'ng',
|
|
390
|
+
description: 'Network Group ID or label',
|
|
391
|
+
parser: Parsers.ngIdOrLabel,
|
|
392
|
+
// complete: NetworkGroup('xxx'),
|
|
393
|
+
}),
|
|
394
|
+
ngDescription: cliparse.option('description', {
|
|
395
|
+
required: true,
|
|
396
|
+
metavar: 'ng_description',
|
|
397
|
+
description: 'Network Group description',
|
|
398
|
+
}),
|
|
399
|
+
ngMemberId: cliparse.option('member-id', {
|
|
400
|
+
aliases: ['m'],
|
|
401
|
+
required: true,
|
|
402
|
+
metavar: 'member_id',
|
|
403
|
+
description: `The member ID: an app ID (i.e. ${Formatter.formatCode('app_xxx')}), add-on ID (i.e. ${Formatter.formatCode('addon_xxx')}) or external node category ID`,
|
|
404
|
+
// complete: NetworkGroup('xxx'),
|
|
405
|
+
}),
|
|
406
|
+
ngMemberDomainName: cliparse.option('domain-name', {
|
|
407
|
+
required: true,
|
|
408
|
+
metavar: 'domain_name',
|
|
409
|
+
description: `Member name used in the ${Formatter.formatUrl('<memberName>.m.<ngID>.ng.clever-cloud.com', false)} domain name alias`,
|
|
410
|
+
}),
|
|
411
|
+
ngPeerId: cliparse.option('peer-id', {
|
|
412
|
+
required: true,
|
|
413
|
+
metavar: 'peer_id',
|
|
414
|
+
description: 'The peer ID',
|
|
415
|
+
// complete: NetworkGroup('xxx'),
|
|
416
|
+
}),
|
|
417
|
+
ngPeerRole: cliparse.option('role', {
|
|
418
|
+
required: true,
|
|
419
|
+
metavar: 'peer_role',
|
|
420
|
+
description: `The peer role, (${Formatter.formatString('client')} or ${Formatter.formatString('server')})`,
|
|
421
|
+
parser: Parsers.ngPeerRole,
|
|
422
|
+
complete: NetworkGroup('listAvailablePeerRoles'),
|
|
423
|
+
}),
|
|
424
|
+
// FIXME: Add "internal" member type
|
|
425
|
+
ngMemberType: cliparse.option('type', {
|
|
426
|
+
required: true,
|
|
427
|
+
metavar: 'member_type',
|
|
428
|
+
description: `The member type (${Formatter.formatString('application')}, ${Formatter.formatString('addon')} or ${Formatter.formatString('external')})`,
|
|
429
|
+
parser: Parsers.ngMemberType,
|
|
430
|
+
complete: NetworkGroup('listAvailableMemberTypes'),
|
|
431
|
+
}),
|
|
432
|
+
ngMemberLabel: cliparse.option('label', {
|
|
433
|
+
required: true,
|
|
434
|
+
metavar: 'member_label',
|
|
435
|
+
description: 'Network Group member label',
|
|
436
|
+
}),
|
|
437
|
+
ngNodeCategoryId: cliparse.option('node-category-id', {
|
|
438
|
+
required: true,
|
|
439
|
+
aliases: ['c'],
|
|
440
|
+
metavar: 'node_category_id',
|
|
441
|
+
description: 'The external node category ID',
|
|
442
|
+
// complete: NetworkGroup('xxx'),
|
|
443
|
+
}),
|
|
444
|
+
ngPeerLabel: cliparse.option('label', {
|
|
445
|
+
required: true,
|
|
446
|
+
metavar: 'peer_label',
|
|
447
|
+
description: 'Network Group peer label',
|
|
448
|
+
}),
|
|
449
|
+
ngPeerParentMemberId: cliparse.option('parent', {
|
|
450
|
+
required: true,
|
|
451
|
+
metavar: 'member_id',
|
|
452
|
+
description: 'Network Group peer category ID (parent member ID)',
|
|
453
|
+
// complete: NetworkGroup('xxx'),
|
|
454
|
+
}),
|
|
455
|
+
optNgIdOrLabel: cliparse.option('ng', {
|
|
456
|
+
required: false,
|
|
457
|
+
metavar: 'ng',
|
|
458
|
+
description: 'Network Group ID or label',
|
|
459
|
+
parser: Parsers.ngIdOrLabel,
|
|
460
|
+
// complete: NetworkGroup('xxx'),
|
|
461
|
+
}),
|
|
462
|
+
optNgMemberLabel: cliparse.option('label', {
|
|
463
|
+
required: false,
|
|
464
|
+
metavar: 'member_label',
|
|
465
|
+
description: 'The member label',
|
|
466
|
+
}),
|
|
467
|
+
optNgNodeCategoryId: cliparse.option('node-category-id', {
|
|
468
|
+
required: false,
|
|
469
|
+
aliases: ['c'],
|
|
470
|
+
metavar: 'node_category_id',
|
|
471
|
+
description: 'The external node category ID',
|
|
472
|
+
// complete: NetworkGroup('xxx'),
|
|
473
|
+
}),
|
|
474
|
+
optNgPeerId: cliparse.option('peer-id', {
|
|
475
|
+
required: false,
|
|
476
|
+
metavar: 'peer_id',
|
|
477
|
+
description: 'The peer ID',
|
|
478
|
+
// complete: NetworkGroup('xxx'),
|
|
479
|
+
}),
|
|
480
|
+
optNgPeerRole: cliparse.option('role', {
|
|
481
|
+
required: false,
|
|
482
|
+
default: 'client',
|
|
483
|
+
metavar: 'peer_role',
|
|
484
|
+
description: `The peer role, (${Formatter.formatString('client')} or ${Formatter.formatString('server')})`,
|
|
485
|
+
parser: Parsers.ngPeerRole,
|
|
486
|
+
complete: NetworkGroup('listAvailablePeerRoles'),
|
|
487
|
+
}),
|
|
488
|
+
optNgSearchAppId: cliparse.option('app-id', {
|
|
489
|
+
required: false,
|
|
490
|
+
metavar: 'app_id',
|
|
491
|
+
description: 'The app id to search',
|
|
492
|
+
// complete: NetworkGroup('xxx'),
|
|
493
|
+
}),
|
|
494
|
+
wgPublicKey: cliparse.option('public-key', {
|
|
495
|
+
required: true,
|
|
496
|
+
metavar: 'public_key',
|
|
497
|
+
description: 'A WireGuard® public key',
|
|
498
|
+
}),
|
|
499
|
+
optWgPrivateKey: cliparse.option('private-key', {
|
|
500
|
+
required: false,
|
|
501
|
+
metavar: 'private_key',
|
|
502
|
+
description: 'A WireGuard® private key',
|
|
503
|
+
}),
|
|
504
|
+
jsonFormat: cliparse.flag('json', { aliases: ['j'], description: 'Show result in JSON format' }),
|
|
505
|
+
tag: cliparse.option('tag', {
|
|
506
|
+
required: true,
|
|
507
|
+
metavar: 'tag',
|
|
508
|
+
description: 'A tag',
|
|
509
|
+
parser: Parsers.tag,
|
|
510
|
+
}),
|
|
511
|
+
tags: cliparse.option('tags', {
|
|
512
|
+
required: true,
|
|
513
|
+
metavar: 'tags',
|
|
514
|
+
description: 'List of tags separated by a comma',
|
|
515
|
+
parser: Parsers.tags,
|
|
516
|
+
}),
|
|
517
|
+
optTags: cliparse.option('tags', {
|
|
518
|
+
metavar: 'tags',
|
|
519
|
+
description: 'List of tags separated by a comma',
|
|
520
|
+
parser: Parsers.tags,
|
|
521
|
+
}),
|
|
522
|
+
optIpAddress: cliparse.option('ip', {
|
|
523
|
+
required: false,
|
|
524
|
+
metavar: 'ip_address',
|
|
525
|
+
description: 'An IP address',
|
|
526
|
+
parser: Parsers.ipAddress,
|
|
527
|
+
}),
|
|
528
|
+
optPortNumber: cliparse.option('port', {
|
|
529
|
+
required: false,
|
|
530
|
+
metavar: 'port_number',
|
|
531
|
+
description: 'A port number',
|
|
532
|
+
parser: Parsers.portNumber,
|
|
533
|
+
}),
|
|
369
534
|
};
|
|
370
535
|
|
|
371
536
|
// ACCESSLOGS COMMAND
|
|
@@ -416,7 +581,7 @@ function run () {
|
|
|
416
581
|
const applications = lazyRequirePromiseModule('../src/commands/applications.js');
|
|
417
582
|
const applicationsCommand = cliparse.command('applications', {
|
|
418
583
|
description: 'List linked applications',
|
|
419
|
-
options: [opts.onlyAliases],
|
|
584
|
+
options: [opts.onlyAliases, opts.jsonFormat],
|
|
420
585
|
}, applications('list'));
|
|
421
586
|
|
|
422
587
|
// CANCEL DEPLOY COMMAND
|
|
@@ -586,6 +751,84 @@ function run () {
|
|
|
586
751
|
args: [args.alias],
|
|
587
752
|
}, makeDefault('makeDefault'));
|
|
588
753
|
|
|
754
|
+
// NETWORK GROUPS COMMANDS
|
|
755
|
+
const networkgroups = lazyRequirePromiseModule('../src/commands/networkgroups/commands.js');
|
|
756
|
+
|
|
757
|
+
// network group category - start
|
|
758
|
+
const networkGroupsListCommand = cliparse.command('list', {
|
|
759
|
+
description: 'List Network Groups with their labels',
|
|
760
|
+
options: [opts.jsonFormat],
|
|
761
|
+
}, networkgroups('listNetworkGroups'));
|
|
762
|
+
const networkGroupsCreateCommand = cliparse.command('create', {
|
|
763
|
+
description: 'Create a Network Group',
|
|
764
|
+
options: [opts.ngLabel, opts.ngDescription, opts.optTags, opts.jsonFormat],
|
|
765
|
+
}, networkgroups('createNg'));
|
|
766
|
+
const networkGroupsDeleteCommand = cliparse.command('delete', {
|
|
767
|
+
description: 'Delete a Network Group',
|
|
768
|
+
options: [opts.ngIdOrLabel],
|
|
769
|
+
}, networkgroups('deleteNg'));
|
|
770
|
+
// network group category - end
|
|
771
|
+
|
|
772
|
+
// member category - start
|
|
773
|
+
const networkGroupsMemberListCommand = cliparse.command('list', {
|
|
774
|
+
description: 'List members of a Network Group',
|
|
775
|
+
// Add option opts.optNgSearchAppId ?
|
|
776
|
+
options: [opts.ngIdOrLabel, opts.naturalName, opts.jsonFormat],
|
|
777
|
+
}, networkgroups('listMembers'));
|
|
778
|
+
const networkGroupsMemberGetCommand = cliparse.command('get', {
|
|
779
|
+
description: 'Get a Network Group member details',
|
|
780
|
+
options: [opts.ngIdOrLabel, opts.ngMemberId, opts.naturalName, opts.jsonFormat],
|
|
781
|
+
}, networkgroups('getMember'));
|
|
782
|
+
const networkGroupsMemberAddCommand = cliparse.command('add', {
|
|
783
|
+
description: 'Add an app or addon as a Network Group member',
|
|
784
|
+
options: [opts.ngIdOrLabel, opts.ngMemberId, opts.ngMemberType, opts.ngMemberDomainName, opts.optNgMemberLabel],
|
|
785
|
+
}, networkgroups('addMember'));
|
|
786
|
+
const networkGroupsMemberRemoveCommand = cliparse.command('remove', {
|
|
787
|
+
description: 'Remove an app or addon from a Network Group',
|
|
788
|
+
options: [opts.ngIdOrLabel, opts.ngMemberId],
|
|
789
|
+
}, networkgroups('removeMember'));
|
|
790
|
+
|
|
791
|
+
const networkGroupsMembersCategoryCommand = cliparse.command('members', {
|
|
792
|
+
description: 'List commands for interacting with Network Group members',
|
|
793
|
+
commands: [networkGroupsMemberListCommand, networkGroupsMemberGetCommand, networkGroupsMemberAddCommand, networkGroupsMemberRemoveCommand],
|
|
794
|
+
});
|
|
795
|
+
// member category - end
|
|
796
|
+
|
|
797
|
+
// peer category - start
|
|
798
|
+
const networkGroupsPeerListCommand = cliparse.command('list', {
|
|
799
|
+
description: 'List peers of a Network Group',
|
|
800
|
+
options: [opts.ngIdOrLabel, opts.jsonFormat],
|
|
801
|
+
}, networkgroups('listPeers'));
|
|
802
|
+
const networkGroupsPeerGetCommand = cliparse.command('get', {
|
|
803
|
+
description: 'Get a Network Group peer details',
|
|
804
|
+
options: [opts.ngIdOrLabel, opts.ngPeerId, opts.jsonFormat],
|
|
805
|
+
}, networkgroups('getPeer'));
|
|
806
|
+
const networkGroupsPeerAddCommand = cliparse.command('add-external', {
|
|
807
|
+
description: 'Add an external node as a Network Group peer',
|
|
808
|
+
options: [opts.ngIdOrLabel, opts.ngPeerRole, opts.wgPublicKey, opts.ngPeerLabel, opts.ngPeerParentMemberId],
|
|
809
|
+
}, networkgroups('addExternalPeer'));
|
|
810
|
+
const networkGroupsPeerRemoveExternalCommand = cliparse.command('remove-external', {
|
|
811
|
+
description: 'Remove an external node from a Network Group',
|
|
812
|
+
options: [opts.ngIdOrLabel, opts.ngPeerId],
|
|
813
|
+
}, networkgroups('removeExternalPeer'));
|
|
814
|
+
|
|
815
|
+
const networkGroupsPeersCategoryCommand = cliparse.command('peers', {
|
|
816
|
+
description: 'List commands for interacting with Network Group peers',
|
|
817
|
+
commands: [networkGroupsPeerListCommand, networkGroupsPeerGetCommand, networkGroupsPeerAddCommand, networkGroupsPeerRemoveExternalCommand],
|
|
818
|
+
});
|
|
819
|
+
// peer category - end
|
|
820
|
+
|
|
821
|
+
const networkGroupsCommand = cliparse.command('networkgroups', {
|
|
822
|
+
description: 'List Network Group commands',
|
|
823
|
+
options: [opts.orgaIdOrName, opts.alias],
|
|
824
|
+
commands: [networkGroupsListCommand, networkGroupsCreateCommand, networkGroupsDeleteCommand, networkGroupsMembersCategoryCommand, networkGroupsPeersCategoryCommand],
|
|
825
|
+
});
|
|
826
|
+
const ngCommand = cliparse.command('ng', {
|
|
827
|
+
description: `Alias for ${Formatter.formatCommand('clever networkgroups')}`,
|
|
828
|
+
options: [opts.orgaIdOrName, opts.alias],
|
|
829
|
+
commands: [networkGroupsListCommand, networkGroupsCreateCommand, networkGroupsDeleteCommand, networkGroupsMembersCategoryCommand, networkGroupsPeersCategoryCommand],
|
|
830
|
+
});
|
|
831
|
+
|
|
589
832
|
// NOTIFY-EMAIL COMMAND
|
|
590
833
|
const notifyEmail = lazyRequirePromiseModule('../src/commands/notify-email.js');
|
|
591
834
|
const addEmailNotificationCommand = cliparse.command('add', {
|
|
@@ -780,6 +1023,8 @@ function run () {
|
|
|
780
1023
|
logoutCommand,
|
|
781
1024
|
logsCommand,
|
|
782
1025
|
makeDefaultCommand,
|
|
1026
|
+
networkGroupsCommand,
|
|
1027
|
+
ngCommand,
|
|
783
1028
|
openCommand,
|
|
784
1029
|
consoleCommand,
|
|
785
1030
|
profileCommand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clever-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0-beta.2",
|
|
4
4
|
"description": "Command Line Interface for Clever Cloud.",
|
|
5
5
|
"main": "bin/clever.js",
|
|
6
6
|
"keywords": [
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"author": "Clever Cloud <ci@clever-cloud.com>",
|
|
16
16
|
"license": "Apache-2.0",
|
|
17
17
|
"bin": {
|
|
18
|
-
"clever": "
|
|
19
|
-
"install-clever-completion": "
|
|
20
|
-
"uninstall-clever-completion": "
|
|
18
|
+
"clever": "bin/clever.js",
|
|
19
|
+
"install-clever-completion": "scripts/install-autocomplete.sh",
|
|
20
|
+
"uninstall-clever-completion": "scripts/uninstall-autocomplete.sh"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"bin",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"scripts/*.sh"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@clevercloud/client": "
|
|
28
|
+
"@clevercloud/client": "7.9.0",
|
|
29
29
|
"clf-date": "^0.2.0",
|
|
30
30
|
"cliparse": "^0.3.3",
|
|
31
31
|
"colors": "1.4.0",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"superagent": "^6.1.0",
|
|
43
43
|
"text-table": "^0.2.0",
|
|
44
44
|
"update-notifier": "^5.1.0",
|
|
45
|
+
"uuid": "^8.3.2",
|
|
45
46
|
"ws": "^7.4.6",
|
|
46
47
|
"xdg": "^0.1.1"
|
|
47
48
|
},
|
|
@@ -6,29 +6,38 @@ const AppConfig = require('../models/app_configuration.js');
|
|
|
6
6
|
const Logger = require('../logger.js');
|
|
7
7
|
|
|
8
8
|
async function list (params) {
|
|
9
|
-
const { 'only-aliases': onlyAliases } = params.options;
|
|
9
|
+
const { 'only-aliases': onlyAliases, json } = params.options;
|
|
10
10
|
|
|
11
11
|
const { apps } = await AppConfig.loadApplicationConf();
|
|
12
12
|
|
|
13
|
-
const formattedApps = formatApps(apps, onlyAliases);
|
|
13
|
+
const formattedApps = formatApps(apps, onlyAliases, json);
|
|
14
14
|
Logger.println(formattedApps);
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
function formatApps (apps, onlyAliases) {
|
|
17
|
+
function formatApps (apps, onlyAliases, json) {
|
|
18
18
|
|
|
19
|
-
if (
|
|
20
|
-
|
|
19
|
+
if (json) {
|
|
20
|
+
if (onlyAliases) {
|
|
21
|
+
apps = apps.map((a) => a.alias);
|
|
22
|
+
}
|
|
23
|
+
return JSON.stringify(apps, null, 2);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
if (onlyAliases) {
|
|
27
|
+
return apps.map((a) => a.alias).join('\n');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return apps
|
|
31
|
+
.map((app) =>
|
|
32
|
+
[
|
|
33
|
+
`Application ${app.name}`,
|
|
34
|
+
` alias: ${colors.bold(app.alias)}`,
|
|
35
|
+
` id: ${app.app_id}`,
|
|
36
|
+
` deployment url: ${app.deploy_url}`].join('\n'),
|
|
37
|
+
)
|
|
38
|
+
.join('\n\n');
|
|
39
|
+
}
|
|
21
40
|
}
|
|
22
|
-
|
|
23
|
-
return apps
|
|
24
|
-
.map((app) => {
|
|
25
|
-
return [
|
|
26
|
-
`Application ${app.name}`,
|
|
27
|
-
` alias: ${colors.bold(app.alias)}`,
|
|
28
|
-
` id: ${app.app_id}`,
|
|
29
|
-
` deployment url: ${app.deploy_url}`].join('\n');
|
|
30
|
-
})
|
|
31
|
-
.join('\n\n');
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
module.exports = { list };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ngApi = require('@clevercloud/client/cjs/api/v4/network-group.js');
|
|
4
|
+
|
|
5
|
+
const { sendToApi } = require('../../models/send-to-api.js');
|
|
6
|
+
|
|
7
|
+
const Logger = require('../../logger.js');
|
|
8
|
+
const NetworkGroup = require('../../models/networkgroup.js');
|
|
9
|
+
const Formatter = require('../../models/format-string.js');
|
|
10
|
+
const TableFormatter = require('../../models/format-ng-table.js');
|
|
11
|
+
|
|
12
|
+
async function listNetworkGroups(params) {
|
|
13
|
+
const { org: orgaIdOrName, alias, json } = params.options;
|
|
14
|
+
const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
|
|
15
|
+
|
|
16
|
+
Logger.info(`Listing Network Groups from owner ${Formatter.formatString(ownerId)}`);
|
|
17
|
+
const result = await ngApi.listNetworkGroups({ ownerId }).then(sendToApi);
|
|
18
|
+
|
|
19
|
+
if (json) {
|
|
20
|
+
Logger.println(JSON.stringify(result, null, 2));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
if (result.length === 0) {
|
|
24
|
+
Logger.println(`No Network Group found for ${ownerId}. You can create one with ${Formatter.formatCommand('clever networkgroups create')}.`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
TableFormatter.printNetworkGroupsTableHeader();
|
|
28
|
+
result
|
|
29
|
+
.map((ng) => TableFormatter.formatNetworkGroupsLine(ng))
|
|
30
|
+
.forEach((ng) => Logger.println(ng));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function createNg(params) {
|
|
36
|
+
const { org: orgaIdOrName, alias, label, description, tags, json } = params.options;
|
|
37
|
+
const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
|
|
38
|
+
|
|
39
|
+
Logger.info(`Creating Network Group from owner ${Formatter.formatString(ownerId)}`);
|
|
40
|
+
const body = { ownerId: ownerId, label, description, tags };
|
|
41
|
+
Logger.debug('Sending body: ' + JSON.stringify(body, null, 2));
|
|
42
|
+
const result = await ngApi.createNetworkGroup({ ownerId }, body).then(sendToApi);
|
|
43
|
+
|
|
44
|
+
if (json) {
|
|
45
|
+
Logger.println(JSON.stringify(result, null, 2));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
Logger.println(`Network Group ${Formatter.formatString(label)} creation will be performed asynchronously.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function deleteNg(params) {
|
|
53
|
+
const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel } = params.options;
|
|
54
|
+
const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
|
|
55
|
+
const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
|
|
56
|
+
|
|
57
|
+
Logger.info(`Deleting Network Group ${Formatter.formatString(networkGroupId)} from owner ${Formatter.formatString(ownerId)}`);
|
|
58
|
+
await ngApi.deleteNetworkGroup({ ownerId, networkGroupId }).then(sendToApi);
|
|
59
|
+
|
|
60
|
+
Logger.println(`Network Group ${Formatter.formatString(networkGroupId)} deletion will be performed asynchronously.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
listNetworkGroups,
|
|
65
|
+
createNg,
|
|
66
|
+
deleteNg,
|
|
67
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ngApi = require('@clevercloud/client/cjs/api/v4/network-group.js');
|
|
4
|
+
|
|
5
|
+
const { sendToApi } = require('../../models/send-to-api.js');
|
|
6
|
+
|
|
7
|
+
const Logger = require('../../logger.js');
|
|
8
|
+
const NetworkGroup = require('../../models/networkgroup.js');
|
|
9
|
+
const Formatter = require('../../models/format-string.js');
|
|
10
|
+
const TableFormatter = require('../../models/format-ng-table.js');
|
|
11
|
+
|
|
12
|
+
async function listMembers(params) {
|
|
13
|
+
const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'natural-name': naturalName, json } = params.options;
|
|
14
|
+
const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
|
|
15
|
+
const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
|
|
16
|
+
|
|
17
|
+
Logger.info(`Listing members from Network Group '${networkGroupId}'`);
|
|
18
|
+
const result = await ngApi.listNetworkGroupMembers({ ownerId, networkGroupId }).then(sendToApi);
|
|
19
|
+
|
|
20
|
+
if (json) {
|
|
21
|
+
Logger.println(JSON.stringify(result, null, 2));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
if (result.length === 0) {
|
|
25
|
+
Logger.println(`No member found. You can add one with ${Formatter.formatCommand('clever networkgroups members add')}.`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
await TableFormatter.printMembersTableHeader(naturalName);
|
|
29
|
+
for (const ng of result) {
|
|
30
|
+
Logger.println(await TableFormatter.formatMembersLine(ng, naturalName));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getMember(params) {
|
|
37
|
+
const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'member-id': memberId, 'natural-name': naturalName, json } = params.options;
|
|
38
|
+
const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
|
|
39
|
+
const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);;
|
|
40
|
+
|
|
41
|
+
Logger.info(`Getting details for member ${Formatter.formatString(memberId)} in Network Group ${Formatter.formatString(networkGroupId)}`);
|
|
42
|
+
const result = await ngApi.getNetworkGroupMember({ ownerId, networkGroupId, memberId: memberId }).then(sendToApi);
|
|
43
|
+
|
|
44
|
+
if (json) {
|
|
45
|
+
Logger.println(JSON.stringify(result, null, 2));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
await TableFormatter.printMembersTableHeader(naturalName);
|
|
49
|
+
Logger.println(await TableFormatter.formatMembersLine(result, naturalName));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function addMember(params) {
|
|
54
|
+
const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'member-id': memberId, type, 'domain-name': domainName, label } = params.options;
|
|
55
|
+
const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
|
|
56
|
+
const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
|
|
57
|
+
|
|
58
|
+
const body = { id: memberId, label, domain_name: domainName, type };
|
|
59
|
+
Logger.debug('Sending body: ' + JSON.stringify(body, null, 2));
|
|
60
|
+
await ngApi.createNetworkGroupMember({ ownerId, networkGroupId }, body).then(sendToApi);
|
|
61
|
+
|
|
62
|
+
Logger.println(`Successfully added member ${Formatter.formatString(memberId)} to Network Group ${Formatter.formatString(networkGroupId)}.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function removeMember(params) {
|
|
66
|
+
const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'member-id': memberId } = params.options;
|
|
67
|
+
const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
|
|
68
|
+
const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
|
|
69
|
+
|
|
70
|
+
await ngApi.deleteNetworkGroupMember({ ownerId, networkGroupId, memberId }).then(sendToApi);
|
|
71
|
+
|
|
72
|
+
Logger.println(`Successfully removed member ${Formatter.formatString(memberId)} from Network Group ${Formatter.formatString(networkGroupId)}.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
listMembers,
|
|
77
|
+
getMember,
|
|
78
|
+
addMember,
|
|
79
|
+
removeMember,
|
|
80
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ngApi = require('@clevercloud/client/cjs/api/v4/network-group.js');
|
|
4
|
+
|
|
5
|
+
const { sendToApi } = require('../../models/send-to-api.js');
|
|
6
|
+
|
|
7
|
+
const Logger = require('../../logger.js');
|
|
8
|
+
const NetworkGroup = require('../../models/networkgroup.js');
|
|
9
|
+
const Formatter = require('../../models/format-string.js');
|
|
10
|
+
const TableFormatter = require('../../models/format-ng-table.js');
|
|
11
|
+
|
|
12
|
+
async function listPeers(params) {
|
|
13
|
+
const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, json } = params.options;
|
|
14
|
+
const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
|
|
15
|
+
const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
|
|
16
|
+
|
|
17
|
+
Logger.info(`Listing peers from Network Group ${Formatter.formatString(networkGroupId)}`);
|
|
18
|
+
const result = await ngApi.listNetworkGroupPeers({ ownerId, networkGroupId }).then(sendToApi);
|
|
19
|
+
|
|
20
|
+
if (json) {
|
|
21
|
+
Logger.println(JSON.stringify(result, null, 2));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
if (result.length === 0) {
|
|
25
|
+
Logger.println(`No peer found. You can add an external one with ${Formatter.formatCommand('clever networkgroups peers add-external')}.`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
TableFormatter.printPeersTableHeader();
|
|
29
|
+
result.forEach((peer) => {
|
|
30
|
+
Logger.println(TableFormatter.formatPeersLine(peer));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getPeer(params) {
|
|
37
|
+
const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'peer-id': peerId, json } = params.options;
|
|
38
|
+
const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
|
|
39
|
+
const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
|
|
40
|
+
|
|
41
|
+
Logger.info(`Getting details for peer ${Formatter.formatString(peerId)} in Network Group ${Formatter.formatString(networkGroupId)}`);
|
|
42
|
+
const peer = await ngApi.getNetworkGroupPeer({ ownerId, networkGroupId, peerId }).then(sendToApi);
|
|
43
|
+
|
|
44
|
+
if (json) {
|
|
45
|
+
Logger.println(JSON.stringify(peer, null, 2));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
TableFormatter.printPeersTableHeader();
|
|
49
|
+
Logger.println(TableFormatter.formatPeersLine(peer));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function addExternalPeer(params) {
|
|
54
|
+
const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, role, 'public-key': publicKey, label, parent, ip, port } = params.options;
|
|
55
|
+
const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
|
|
56
|
+
const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
|
|
57
|
+
|
|
58
|
+
const body = { peer_role: role, public_key: publicKey, label, parent_member: parent, ip, port };
|
|
59
|
+
Logger.info(`Adding external peer to Network Group ${Formatter.formatString(networkGroupId)}`);
|
|
60
|
+
Logger.debug('Sending body: ' + JSON.stringify(body, null, 2));
|
|
61
|
+
const { id: peerId } = await ngApi.createNetworkGroupExternalPeer({ ownerId, networkGroupId }, body).then(sendToApi);
|
|
62
|
+
|
|
63
|
+
Logger.println(`External peer ${Formatter.formatString(peerId)} must have been added to Network Group ${Formatter.formatString(networkGroupId)}.`);
|
|
64
|
+
return peerId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function removeExternalPeer(params) {
|
|
68
|
+
const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'peer-id': peerId } = params.options;
|
|
69
|
+
const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
|
|
70
|
+
const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
|
|
71
|
+
|
|
72
|
+
Logger.info(`Removing external peer ${Formatter.formatString(peerId)} from Network Group ${Formatter.formatString(networkGroupId)}`);
|
|
73
|
+
await ngApi.deleteNetworkGroupExternalPeer({ ownerId, networkGroupId, peerId }).then(sendToApi);
|
|
74
|
+
|
|
75
|
+
Logger.println(`External peer ${Formatter.formatString(peerId)} must have been removed from Network Group ${Formatter.formatString(networkGroupId)}.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
listPeers,
|
|
80
|
+
getPeer,
|
|
81
|
+
addExternalPeer,
|
|
82
|
+
removeExternalPeer,
|
|
83
|
+
};
|
|
@@ -79,6 +79,14 @@ function findApp (config, alias) {
|
|
|
79
79
|
return appByAlias;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
return findDefaultApp(config);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findDefaultApp (config) {
|
|
86
|
+
if (_.isEmpty(config.apps)) {
|
|
87
|
+
throw new Error('There are no applications linked. You can add one with `clever link`');
|
|
88
|
+
}
|
|
89
|
+
|
|
82
90
|
if (config.default != null) {
|
|
83
91
|
const defaultApp = _.find(config.apps, { app_id: config.default });
|
|
84
92
|
if (defaultApp == null) {
|
|
@@ -95,6 +103,25 @@ function findApp (config, alias) {
|
|
|
95
103
|
throw new Error(`Several applications are linked. You can specify one with the "--alias" option. Run "clever applications" to list linked applications. Available aliases: ${aliases}`);
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
async function getAppDetailsForId (appId) {
|
|
107
|
+
const config = await loadApplicationConf();
|
|
108
|
+
|
|
109
|
+
if (_.isEmpty(config.apps)) {
|
|
110
|
+
throw new Error('There are no applications linked. You can add one with `clever link`');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const [appById, secondAppById] = _.filter(config.apps, { app_id: appId });
|
|
114
|
+
if (appById == null) {
|
|
115
|
+
throw new Error(`There are no applications matching id '${appId}'`);
|
|
116
|
+
}
|
|
117
|
+
if (secondAppById != null) {
|
|
118
|
+
throw new Error(`There are several applications matching id '${appId}'.`
|
|
119
|
+
+ 'This should not happen, your `.clever.json` should be fixed.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return appById;
|
|
123
|
+
}
|
|
124
|
+
|
|
98
125
|
async function getAppDetails ({ alias }) {
|
|
99
126
|
const config = await loadApplicationConf();
|
|
100
127
|
const app = findApp(config, alias);
|
|
@@ -110,6 +137,16 @@ async function getAppDetails ({ alias }) {
|
|
|
110
137
|
};
|
|
111
138
|
};
|
|
112
139
|
|
|
140
|
+
async function getMostNaturalName (appId) {
|
|
141
|
+
try {
|
|
142
|
+
const details = await getAppDetailsForId(appId);
|
|
143
|
+
return details.alias || details.name || appId;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return appId;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
113
150
|
function persistConfig (modifiedConfig) {
|
|
114
151
|
const jsonContents = JSON.stringify(modifiedConfig, null, 2);
|
|
115
152
|
return fs.writeFile(conf.APP_CONFIGURATION_FILE, jsonContents);
|
|
@@ -128,5 +165,6 @@ module.exports = {
|
|
|
128
165
|
removeLinkedApplication,
|
|
129
166
|
findApp,
|
|
130
167
|
getAppDetails,
|
|
168
|
+
getMostNaturalName,
|
|
131
169
|
setDefault,
|
|
132
170
|
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const formatNgTable = require('../format-table.js');
|
|
4
|
+
const colors = require('colors/safe');
|
|
5
|
+
|
|
6
|
+
const AppConfig = require('./app_configuration.js');
|
|
7
|
+
const Logger = require('../logger.js');
|
|
8
|
+
const Formatter = require('./format-string.js');
|
|
9
|
+
|
|
10
|
+
function printSeparator (columnLengths) {
|
|
11
|
+
Logger.println('─'.repeat(columnLengths.reduce((a, b) => a + b + 2)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// We use examples of maximum width text to have a clean display
|
|
15
|
+
const networkGroupsTableColumnLengths = [
|
|
16
|
+
39, /* id length */
|
|
17
|
+
48, /* label length */
|
|
18
|
+
7, /* members length */
|
|
19
|
+
5, /* peers length */
|
|
20
|
+
48, /* description */
|
|
21
|
+
];
|
|
22
|
+
const formatNetworkGroupsTable = formatNgTable(networkGroupsTableColumnLengths);
|
|
23
|
+
function formatNetworkGroupsLine (ng) {
|
|
24
|
+
return formatNetworkGroupsTable([
|
|
25
|
+
[
|
|
26
|
+
Formatter.formatId(ng.id),
|
|
27
|
+
Formatter.formatString(ng.label, false),
|
|
28
|
+
Formatter.formatNumber(ng.members.length),
|
|
29
|
+
Formatter.formatNumber(ng.peers.length),
|
|
30
|
+
Formatter.formatString(ng.description || ' ', false),
|
|
31
|
+
],
|
|
32
|
+
]);
|
|
33
|
+
};
|
|
34
|
+
function printNetworkGroupsTableHeader () {
|
|
35
|
+
Logger.println(colors.bold(formatNetworkGroupsTable([
|
|
36
|
+
['Network Group ID', 'Label', 'Members', 'Peers', 'Description'],
|
|
37
|
+
])));
|
|
38
|
+
printSeparator(networkGroupsTableColumnLengths);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const membersTableColumnLengths = [
|
|
42
|
+
48, /* id length */
|
|
43
|
+
12, /* type length */
|
|
44
|
+
48, /* label length */
|
|
45
|
+
24, /* domain-name length */
|
|
46
|
+
];
|
|
47
|
+
const formatMembersTable = formatNgTable(membersTableColumnLengths);
|
|
48
|
+
async function formatMembersLine (member, showAliases = false) {
|
|
49
|
+
return formatMembersTable([
|
|
50
|
+
[
|
|
51
|
+
showAliases
|
|
52
|
+
? Formatter.formatString(await AppConfig.getMostNaturalName(member.id), false)
|
|
53
|
+
: Formatter.formatId(member.id),
|
|
54
|
+
Formatter.formatString(member.type, false),
|
|
55
|
+
Formatter.formatString(member.label, false),
|
|
56
|
+
Formatter.formatString(member.domain_name || ' ', false),
|
|
57
|
+
],
|
|
58
|
+
]);
|
|
59
|
+
};
|
|
60
|
+
async function printMembersTableHeader (naturalName = false) {
|
|
61
|
+
Logger.println(colors.bold(formatMembersTable([
|
|
62
|
+
[
|
|
63
|
+
naturalName ? 'Member' : 'Member ID',
|
|
64
|
+
'Member Type',
|
|
65
|
+
'Label',
|
|
66
|
+
'Domain Name',
|
|
67
|
+
],
|
|
68
|
+
])));
|
|
69
|
+
printSeparator(membersTableColumnLengths);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const peersTableColumnLengths = [
|
|
73
|
+
45, /* id length */
|
|
74
|
+
12, /* type length */
|
|
75
|
+
14, /* endpoint type length */
|
|
76
|
+
48, /* label length */
|
|
77
|
+
24, /* hostname */
|
|
78
|
+
15, /* ip */
|
|
79
|
+
];
|
|
80
|
+
const formatPeersTable = formatNgTable(peersTableColumnLengths);
|
|
81
|
+
function formatPeersLine (peer) {
|
|
82
|
+
const ip = (peer.endpoint.type === 'ServerEndpoint') ? peer.endpoint.ng_term.ip : peer.endpoint.ng_ip;
|
|
83
|
+
return formatPeersTable([
|
|
84
|
+
[
|
|
85
|
+
Formatter.formatId(peer.id),
|
|
86
|
+
Formatter.formatString(peer.type, false),
|
|
87
|
+
Formatter.formatString(peer.endpoint.type, false),
|
|
88
|
+
Formatter.formatString(peer.label, false),
|
|
89
|
+
Formatter.formatString(peer.hostname, false),
|
|
90
|
+
Formatter.formatIp(ip),
|
|
91
|
+
],
|
|
92
|
+
]);
|
|
93
|
+
};
|
|
94
|
+
function printPeersTableHeader () {
|
|
95
|
+
Logger.println(colors.bold(formatPeersTable([
|
|
96
|
+
[
|
|
97
|
+
'Peer ID',
|
|
98
|
+
'Peer Type',
|
|
99
|
+
'Endpoint Type',
|
|
100
|
+
'Label',
|
|
101
|
+
'Hostname',
|
|
102
|
+
'IP Address',
|
|
103
|
+
],
|
|
104
|
+
])));
|
|
105
|
+
printSeparator(peersTableColumnLengths);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
formatNetworkGroupsLine,
|
|
110
|
+
printNetworkGroupsTableHeader,
|
|
111
|
+
formatMembersLine,
|
|
112
|
+
printMembersTableHeader,
|
|
113
|
+
formatPeersLine,
|
|
114
|
+
printPeersTableHeader,
|
|
115
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const colors = require('colors/safe');
|
|
4
|
+
|
|
5
|
+
function formatId (id) {
|
|
6
|
+
return colors.dim(id);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatString (str, decorated = true) {
|
|
10
|
+
const string = decorated ? `'${str}'` : str;
|
|
11
|
+
return colors.green(string);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatNumber (number) {
|
|
15
|
+
return colors.yellow(number);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatIp (ip) {
|
|
19
|
+
return colors.cyan(ip);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatUrl (url, decorated = true) {
|
|
23
|
+
const string = decorated ? `<${url}>` : url;
|
|
24
|
+
return colors.cyan(string);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatCommand (command, decorated = true) {
|
|
28
|
+
const string = decorated ? `\`${command}\`` : command;
|
|
29
|
+
return colors.magenta(string);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatCode (code, decorated = true) {
|
|
33
|
+
return formatCommand(code, decorated);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
formatId,
|
|
38
|
+
formatString,
|
|
39
|
+
formatNumber,
|
|
40
|
+
formatIp,
|
|
41
|
+
formatUrl,
|
|
42
|
+
formatCommand,
|
|
43
|
+
formatCode,
|
|
44
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const autocomplete = require('cliparse').autocomplete;
|
|
4
|
+
const Organisation = require('../models/organisation.js');
|
|
5
|
+
const User = require('../models/user.js');
|
|
6
|
+
const AppConfig = require('./app_configuration.js');
|
|
7
|
+
const ngApi = require('@clevercloud/client/cjs/api/v4/network-group.js');
|
|
8
|
+
const { sendToApi } = require('./send-to-api.js');
|
|
9
|
+
|
|
10
|
+
async function getOwnerId(orgaIdOrName, alias) {
|
|
11
|
+
if (orgaIdOrName == null) {
|
|
12
|
+
try {
|
|
13
|
+
return (await AppConfig.getAppDetails({alias})).ownerId;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return (await User.getCurrentId())
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
return (await Organisation.getId(orgaIdOrName));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function getId(ownerId, ngIdOrLabel) {
|
|
23
|
+
if (ngIdOrLabel == null) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (ngIdOrLabel.ng_id != null) {
|
|
28
|
+
return ngIdOrLabel.ng_id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return getByLabel(ownerId, ngIdOrLabel.ng_label)
|
|
32
|
+
.then((ng) => ng.id);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function getByLabel(owner_id, label) {
|
|
36
|
+
const networkGroups = await ngApi.listNetworkGroups({ owner_id }).then(sendToApi);
|
|
37
|
+
const filteredNgs = networkGroups.filter((ng) => ng.label === label);
|
|
38
|
+
|
|
39
|
+
if (filteredNgs.length === 0) {
|
|
40
|
+
throw new Error('Network Group not found');
|
|
41
|
+
}
|
|
42
|
+
if (filteredNgs.length > 1) {
|
|
43
|
+
throw new Error('Ambiguous Network Group label');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return filteredNgs[0];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function listAvailablePeerRoles() {
|
|
50
|
+
return autocomplete.words(['client', 'server']);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listAvailableMemberTypes() {
|
|
54
|
+
return autocomplete.words(['application', 'addon', 'external']);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
getOwnerId,
|
|
59
|
+
getId,
|
|
60
|
+
listAvailablePeerRoles,
|
|
61
|
+
listAvailableMemberTypes,
|
|
62
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { promises: fs, existsSync } = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const Logger = require('../logger.js');
|
|
8
|
+
const Formatter = require('./format-string.js');
|
|
9
|
+
|
|
10
|
+
function getWgConfFolder () {
|
|
11
|
+
// TODO: See if we can use runtime dirs
|
|
12
|
+
return path.join(os.tmpdir(), 'com.clever-cloud.networkgroups');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function createWgConfFolderIfNeeded () {
|
|
16
|
+
const confFolder = getWgConfFolder();
|
|
17
|
+
if (!existsSync(confFolder)) {
|
|
18
|
+
await fs.mkdir(confFolder);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getWgConfInformation (ngId) {
|
|
23
|
+
const confName = `wgcc${ngId.slice(-8)}`;
|
|
24
|
+
const confPath = path.join(getWgConfFolder(), `${confName}.conf`);
|
|
25
|
+
|
|
26
|
+
return { confName, confPath };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getPeerIdPath (confName) {
|
|
30
|
+
return path.join(getWgConfFolder(), `${confName}.id`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function storePeerId (peerId, confName) {
|
|
34
|
+
const filePath = getPeerIdPath(confName);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await fs.writeFile(filePath, peerId, { mode: 0o600, flag: 'wx' });
|
|
38
|
+
Logger.info(`Saved peer ID file to ${Formatter.formatUrl(filePath)}`);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error(`Error saving peer ID: ${error}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function getPeerId (ngId) {
|
|
46
|
+
const { confName } = getWgConfInformation(ngId);
|
|
47
|
+
const filePath = getPeerIdPath(confName);
|
|
48
|
+
if (existsSync(filePath)) {
|
|
49
|
+
Logger.debug(`Reading peer ID from ${Formatter.formatUrl(filePath)}`);
|
|
50
|
+
return (await fs.readFile(filePath, { encoding: 'utf-8' })).trim();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
Logger.debug(`No file found at ${Formatter.formatUrl(filePath)}`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function deletePeerIdFile (ngId) {
|
|
59
|
+
const { confName } = getWgConfInformation(ngId);
|
|
60
|
+
const filePath = getPeerIdPath(confName);
|
|
61
|
+
// We need `force: true` to avoid errors if file doesn't exist
|
|
62
|
+
await fs.rm(filePath, { force: true });
|
|
63
|
+
Logger.info(`Deleted peer ID from ${Formatter.formatUrl(filePath)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function getInterfaceName (confName) {
|
|
67
|
+
// This file is created by WireGuard®, hence the file path (`/var/run/…`)
|
|
68
|
+
// TODO: Handle Windows (not yet supported by `wg-quick` anyway)
|
|
69
|
+
const interfaceNameFile = path.join('/var', 'run', 'wireguard', `${confName}.name`);
|
|
70
|
+
|
|
71
|
+
Logger.debug(`Reading WireGuard® interface name in ${Formatter.formatUrl(interfaceNameFile)}…`);
|
|
72
|
+
const interfaceName = (await fs.readFile(interfaceNameFile, { encoding: 'utf-8' })).trim();
|
|
73
|
+
Logger.debug(`Found WireGuard® interface name ${Formatter.formatString(interfaceName)} for ${Formatter.formatString(confName)}`);
|
|
74
|
+
return interfaceName;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function confWithoutPlaceholders (conf, { privateKey }) {
|
|
78
|
+
conf = conf.replace('<%PrivateKey%>', privateKey);
|
|
79
|
+
|
|
80
|
+
// TODO: This just removes leading and trailing new lines in the configuration file
|
|
81
|
+
// It should be better formatted on the API's side
|
|
82
|
+
conf = conf.trim();
|
|
83
|
+
|
|
84
|
+
return conf;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
createWgConfFolderIfNeeded,
|
|
89
|
+
getWgConfInformation,
|
|
90
|
+
storePeerId,
|
|
91
|
+
getPeerId,
|
|
92
|
+
deletePeerIdFile,
|
|
93
|
+
getInterfaceName,
|
|
94
|
+
confWithoutPlaceholders,
|
|
95
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync, spawnSync } = require('child_process');
|
|
4
|
+
const Logger = require('../logger.js');
|
|
5
|
+
|
|
6
|
+
function privateKey () {
|
|
7
|
+
return execSync('wg genkey', { encoding: 'utf-8' }).trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function publicKey (privateKey) {
|
|
11
|
+
return execSync(`echo '${privateKey}' | wg pubkey`, { encoding: 'utf-8' }).trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function up (confPath) {
|
|
15
|
+
// We must use `spawn` with `detached: true` instead of `exec`
|
|
16
|
+
// because `wg-quick up` starts a `wireguard-go` used by `wg-quick down`
|
|
17
|
+
const { stdout, stderr } = spawnSync('wg-quick', ['up', confPath], { detached: true, encoding: 'utf-8' });
|
|
18
|
+
if (stdout.length > 0) {
|
|
19
|
+
Logger.debug(stdout.trim());
|
|
20
|
+
}
|
|
21
|
+
if (stderr.length > 0) {
|
|
22
|
+
Logger.debug(stderr.trim());
|
|
23
|
+
}
|
|
24
|
+
Logger.println('Activated WireGuard® tunnel');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function update (confPath, interfaceName) {
|
|
28
|
+
try {
|
|
29
|
+
// Update WireGuard® configuration
|
|
30
|
+
execSync(`wg-quick strip ${confPath} | wg syncconf ${interfaceName} /dev/stdin`);
|
|
31
|
+
Logger.info('Updated WireGuard® tunnel configuration');
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new Error(`Error updating WireGuard® tunnel configuration: ${error}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function down (confPath) {
|
|
39
|
+
const { stdout, stderr } = spawnSync('wg-quick', ['down', confPath], { encoding: 'utf-8' });
|
|
40
|
+
if (stdout.length > 0) {
|
|
41
|
+
Logger.debug(stdout.trim());
|
|
42
|
+
}
|
|
43
|
+
if (stderr.length > 0) {
|
|
44
|
+
Logger.debug(stderr.trim());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check that `wg` and `wg-quick` are installed
|
|
50
|
+
*/
|
|
51
|
+
function checkAvailable () {
|
|
52
|
+
try {
|
|
53
|
+
// The redirect to `/dev/null` ensures that your program does not produce the output of these commands.
|
|
54
|
+
execSync('which wg > /dev/null 2>&1');
|
|
55
|
+
execSync('which wg-quick > /dev/null 2>&1');
|
|
56
|
+
|
|
57
|
+
// TODO: Handle Windows
|
|
58
|
+
// - Those checks won't work on Windows, and wg-quick doesn't exist anyway.
|
|
59
|
+
// - We need to wait for a Windows version of wg-quick to support the rest of the operations
|
|
60
|
+
// - Or we could use vanilla wg on Windows, and wg-quick on other OSs
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
privateKey,
|
|
70
|
+
publicKey,
|
|
71
|
+
up,
|
|
72
|
+
update,
|
|
73
|
+
down,
|
|
74
|
+
checkAvailable,
|
|
75
|
+
};
|
package/src/parsers.js
CHANGED
|
@@ -48,7 +48,7 @@ function appIdOrName (string) {
|
|
|
48
48
|
return cliparse.parsers.success({ app_name: string });
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const orgaIdRegex = /^orga_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
51
|
+
const orgaIdRegex = /^(user_|orga_)[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
52
52
|
|
|
53
53
|
function orgaIdOrName (string) {
|
|
54
54
|
if (string.match(orgaIdRegex)) {
|
|
@@ -66,6 +66,15 @@ function addonIdOrName (string) {
|
|
|
66
66
|
return cliparse.parsers.success({ addon_name: string });
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
const ngIdRegex = /^ng_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
70
|
+
|
|
71
|
+
function ngIdOrLabel (string) {
|
|
72
|
+
if (string.match(ngIdRegex)) {
|
|
73
|
+
return cliparse.parsers.success({ ng_id: string });
|
|
74
|
+
}
|
|
75
|
+
return cliparse.parsers.success({ ng_label: string });
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
function commaSeparated (string) {
|
|
70
79
|
return cliparse.parsers.success(string.split(','));
|
|
71
80
|
}
|
|
@@ -86,6 +95,63 @@ function integer (string) {
|
|
|
86
95
|
return cliparse.parsers.success(integer);
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
// /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/i;
|
|
99
|
+
const tagRegex = /^[^,\s]+$/;
|
|
100
|
+
|
|
101
|
+
function tag (string) {
|
|
102
|
+
if (string.match(tagRegex)) {
|
|
103
|
+
return cliparse.parsers.success(string);
|
|
104
|
+
}
|
|
105
|
+
return cliparse.parsers.error(`Invalid tag '${string}'. Should match ${tagRegex}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function tags (string) {
|
|
109
|
+
if (String(string).length === 0) {
|
|
110
|
+
return cliparse.parsers.success([]);
|
|
111
|
+
}
|
|
112
|
+
const tags = String(string).split(',');
|
|
113
|
+
for (const current of tags) {
|
|
114
|
+
if (tag(current).error) {
|
|
115
|
+
return cliparse.parsers.error(`Invalid tag '${current}'. Should match \`${tagRegex}\``);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return cliparse.parsers.success(tags);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ngMemberType (string) {
|
|
122
|
+
const possible = ['application', 'addon', 'external'];
|
|
123
|
+
if (possible.includes(string)) {
|
|
124
|
+
return cliparse.parsers.success(string);
|
|
125
|
+
}
|
|
126
|
+
return cliparse.parsers.error(`Invalid member type '${string}'. Should be in ${JSON.stringify(possible)}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ngPeerRole (string) {
|
|
130
|
+
const possible = ['client', 'server'];
|
|
131
|
+
if (possible.includes(string)) {
|
|
132
|
+
return cliparse.parsers.success(string);
|
|
133
|
+
}
|
|
134
|
+
return cliparse.parsers.error(`Invalid peer role '${string}'. Should be in ${JSON.stringify(possible)}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const ipAddressRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])$/;
|
|
138
|
+
|
|
139
|
+
function ipAddress (string) {
|
|
140
|
+
if (string.match(ipAddressRegex)) {
|
|
141
|
+
return cliparse.parsers.success(string);
|
|
142
|
+
}
|
|
143
|
+
return cliparse.parsers.error(`Invalid IP address '${string}'. Should match ${ipAddressRegex}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const portNumberRegex = /^\d{1,5}$/;
|
|
147
|
+
|
|
148
|
+
function portNumber (number) {
|
|
149
|
+
if (String(number).match(portNumberRegex)) {
|
|
150
|
+
return cliparse.parsers.success(number);
|
|
151
|
+
}
|
|
152
|
+
return cliparse.parsers.error(`Invalid port number '${number}'. Should match ${portNumberRegex}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
89
155
|
module.exports = {
|
|
90
156
|
buildFlavor,
|
|
91
157
|
flavor,
|
|
@@ -94,7 +160,16 @@ module.exports = {
|
|
|
94
160
|
appIdOrName,
|
|
95
161
|
orgaIdOrName,
|
|
96
162
|
addonIdOrName,
|
|
163
|
+
ngIdOrLabel,
|
|
97
164
|
commaSeparated,
|
|
98
165
|
accessLogsFormat,
|
|
99
166
|
integer,
|
|
167
|
+
tag,
|
|
168
|
+
tags,
|
|
169
|
+
ngMemberType,
|
|
170
|
+
ngPeerRole,
|
|
171
|
+
ipAddressRegex,
|
|
172
|
+
ipAddress,
|
|
173
|
+
portNumberRegex,
|
|
174
|
+
portNumber,
|
|
100
175
|
};
|