clever-tools 3.11.0 → 3.12.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 +140 -1
- package/package.json +4 -3
- package/src/clever-client/auth-bridge.js +61 -0
- package/src/clever-client/ng.js +18 -0
- package/src/commands/addon.js +22 -21
- package/src/commands/config.js +3 -3
- package/src/commands/curl.js +17 -17
- package/src/commands/database.js +9 -9
- package/src/commands/domain.js +13 -16
- package/src/commands/env.js +15 -8
- package/src/commands/kv.js +1 -1
- package/src/commands/link.js +4 -2
- package/src/commands/ng.js +202 -0
- package/src/commands/profile.js +9 -6
- package/src/commands/published-config.js +7 -7
- package/src/commands/stop.js +2 -2
- package/src/commands/tcp-redirs.js +5 -5
- package/src/commands/tokens.js +137 -0
- package/src/experimental-features.js +46 -2
- package/src/lib/ng-print.js +195 -0
- package/src/logger.js +3 -0
- package/src/models/activity.js +2 -3
- package/src/models/addon.js +4 -4
- package/src/models/application.js +26 -16
- package/src/models/configuration.js +1 -0
- package/src/models/namespaces.js +3 -2
- package/src/models/ng-resources.js +270 -0
- package/src/models/ng.js +276 -0
- package/src/models/send-to-api.js +18 -0
- package/src/parsers.js +57 -3
- package/src/prompt-password.js +10 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import colors from 'colors/safe.js';
|
|
2
|
+
import * as networkGroup from '../models/ng.js';
|
|
3
|
+
|
|
4
|
+
import { Logger } from '../logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Print a Network Group
|
|
8
|
+
* @param {Object} ng The Network Group to print
|
|
9
|
+
* @param {string} format Output format
|
|
10
|
+
* @param {boolean} full If true, get more details about the Network Group (default: false)
|
|
11
|
+
*/
|
|
12
|
+
function printNg (ng, format, full = false) {
|
|
13
|
+
|
|
14
|
+
switch (format) {
|
|
15
|
+
case 'json': {
|
|
16
|
+
Logger.printJson(ng);
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case 'human':
|
|
20
|
+
default: {
|
|
21
|
+
const ngData = {
|
|
22
|
+
ID: ng.id,
|
|
23
|
+
Label: ng.label,
|
|
24
|
+
Description: ng.description,
|
|
25
|
+
Network: `${ng.networkIp}`,
|
|
26
|
+
'Members/Peers': `${Object.keys(ng.members)?.length}/${Object.keys(ng.peers)?.length}`,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
console.table(ngData);
|
|
30
|
+
|
|
31
|
+
if (full) {
|
|
32
|
+
const members = Object.entries(ng.members)
|
|
33
|
+
.sort((a, b) => a[1].domainName.localeCompare(b[1].domainName))
|
|
34
|
+
.map(([id, member]) => ({
|
|
35
|
+
Domain: member.domainName,
|
|
36
|
+
}));
|
|
37
|
+
if (members.length > 0) {
|
|
38
|
+
Logger.println(colors.bold(' • Members:'));
|
|
39
|
+
console.table(members);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const peers = Object.entries(ng.peers)
|
|
43
|
+
.sort((a, b) => a[1].parentMember.localeCompare(b[1].parentMember))
|
|
44
|
+
.map(([id, peer]) => formatPeer(peer));
|
|
45
|
+
if (peers.length > 0) {
|
|
46
|
+
Logger.println(colors.bold(' • Peers:'));
|
|
47
|
+
console.table(peers);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Print a Network Group member
|
|
56
|
+
* @param {Object} member The Network Group member to print
|
|
57
|
+
* @param {string} format Output format
|
|
58
|
+
*/
|
|
59
|
+
function printMember (member, format) {
|
|
60
|
+
|
|
61
|
+
switch (format) {
|
|
62
|
+
case 'json': {
|
|
63
|
+
Logger.printJson(member);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'human':
|
|
67
|
+
default: {
|
|
68
|
+
console.table({
|
|
69
|
+
Label: member.label,
|
|
70
|
+
Domain: member.domainName,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Print a Network Group peer
|
|
78
|
+
* @param {Object} peer The Network Group peer to print
|
|
79
|
+
* @param {string} format Output format
|
|
80
|
+
* @param {boolean} full If true, get more details about the peer (default: false)
|
|
81
|
+
*/
|
|
82
|
+
function printPeer (peer, format, full = false) {
|
|
83
|
+
switch (format) {
|
|
84
|
+
case 'json': {
|
|
85
|
+
Logger.printJson(peer);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case 'human':
|
|
89
|
+
default: {
|
|
90
|
+
console.table(formatPeer(peer, full));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format a peer to print
|
|
97
|
+
* @param {Object} peer
|
|
98
|
+
* @param {boolean} full If true, get more details about the peer (default: false)
|
|
99
|
+
*/
|
|
100
|
+
function formatPeer (peer, full = false) {
|
|
101
|
+
const peerToPrint = {
|
|
102
|
+
'Parent Member': peer.parentMember,
|
|
103
|
+
ID: peer.id,
|
|
104
|
+
Label: peer.label,
|
|
105
|
+
Type: peer.type,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (full) {
|
|
109
|
+
if (peer.endpoint.ngTerm != null) {
|
|
110
|
+
peerToPrint['Host:IP'] = `${peer.endpoint.ngTerm.host}:${peer.endpoint.ngTerm.port}`;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
peerToPrint.Host = peer.endpoint.ngIp;
|
|
114
|
+
}
|
|
115
|
+
if (peer.endpoint.publicTerm != null) {
|
|
116
|
+
peerToPrint['Public Term'] = `${peer.endpoint.publicTerm.host}:${peer.endpoint.publicTerm.port}`;
|
|
117
|
+
}
|
|
118
|
+
peerToPrint['Public Key'] = peer.publicKey;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return peerToPrint;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Print the results of a search or get action
|
|
125
|
+
* @param {object} idOrLabel ID or label of the Network Group, a member or a peer
|
|
126
|
+
* @param {object} org Organisation ID or name
|
|
127
|
+
* @param {string} format Output format
|
|
128
|
+
* @param {string} action Action to perform (search or get)
|
|
129
|
+
* @param {string} type Type of item to search (NetworkGroup, Member, Peer)
|
|
130
|
+
*/
|
|
131
|
+
export async function printResults (idOrLabel, org, format, action, type) {
|
|
132
|
+
|
|
133
|
+
const exactMatch = action === 'get';
|
|
134
|
+
const toLookFor = type ?? (action === 'search' ? 'all' : 'single');
|
|
135
|
+
|
|
136
|
+
const found = await networkGroup.searchNgOrResource(idOrLabel, org, toLookFor, exactMatch);
|
|
137
|
+
|
|
138
|
+
if (!found.length) {
|
|
139
|
+
const searchString = idOrLabel.ngId
|
|
140
|
+
?? idOrLabel.memberId
|
|
141
|
+
?? idOrLabel.ngResourceLabel;
|
|
142
|
+
Logger.println(`${colors.blue('!')} No Network Group or resource found for ${colors.blue(searchString)}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (found.length === 1) {
|
|
147
|
+
switch (found[0].type) {
|
|
148
|
+
case 'NetworkGroup':
|
|
149
|
+
return printNg(found[0], format, true);
|
|
150
|
+
case 'Member':
|
|
151
|
+
return printMember(found[0], format);
|
|
152
|
+
case 'CleverPeer':
|
|
153
|
+
case 'ExternalPeer':
|
|
154
|
+
return printPeer(found[0], format, true);
|
|
155
|
+
default:
|
|
156
|
+
throw new Error(`Unknown item type: ${found[0].type}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (action === 'search') {
|
|
161
|
+
// Group found items by type in a new object
|
|
162
|
+
const grouped = found.reduce((acc, item) => {
|
|
163
|
+
if (!acc[item.type]) {
|
|
164
|
+
acc[item.type] = [];
|
|
165
|
+
}
|
|
166
|
+
acc[item.type].push(item);
|
|
167
|
+
return acc;
|
|
168
|
+
}, {});
|
|
169
|
+
|
|
170
|
+
switch (format) {
|
|
171
|
+
case 'json': {
|
|
172
|
+
Logger.printJson(grouped);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case 'human':
|
|
176
|
+
default: {
|
|
177
|
+
if (grouped.NetworkGroup) {
|
|
178
|
+
Logger.println(`${colors.bold(` • Found ${grouped.NetworkGroup.length} Network Group(s):`)}`);
|
|
179
|
+
grouped.NetworkGroup?.forEach((item) => printNg(item, format));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (grouped.Member) {
|
|
183
|
+
Logger.println(`${colors.bold(` • Found ${grouped.Member.length} Member(s):`)}`);
|
|
184
|
+
grouped.Member?.forEach((item) => printMember(item, format));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (grouped.ExternalPeer || grouped.CleverPeer) {
|
|
188
|
+
Logger.println(`${colors.bold(` • Found ${grouped.ExternalPeer.length + grouped.CleverPeer.length} Peer(s):`)}`);
|
|
189
|
+
grouped.CleverPeer?.forEach((item) => printPeer(item, format));
|
|
190
|
+
grouped.ExternalPeer?.forEach((item) => printPeer(item, format));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/logger.js
CHANGED
|
@@ -55,6 +55,9 @@ export const Logger = _(['debug', 'info', 'warn', 'error'])
|
|
|
55
55
|
// No decoration for Logger.println
|
|
56
56
|
Logger.println = console.log;
|
|
57
57
|
|
|
58
|
+
// Logger for success with a green check before the message
|
|
59
|
+
Logger.printSuccess = (message) => console.log(`${colors.bold.green('✓')} ${message}`);
|
|
60
|
+
|
|
58
61
|
// No decoration for Logger.println
|
|
59
62
|
Logger.printJson = (obj) => {
|
|
60
63
|
console.log(JSON.stringify(obj, null, 2));
|
package/src/models/activity.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import * as application from '@clevercloud/client/esm/api/v2/application.js';
|
|
2
|
-
|
|
3
1
|
import { sendToApi } from './send-to-api.js';
|
|
2
|
+
import { getAllDeployments } from '@clevercloud/client/esm/api/v2/application.js';
|
|
4
3
|
|
|
5
4
|
export function list (ownerId, appId, showAll) {
|
|
6
5
|
const limit = showAll ? null : 10;
|
|
7
|
-
return
|
|
6
|
+
return getAllDeployments({ id: ownerId, appId, limit }).then(sendToApi);
|
|
8
7
|
};
|
package/src/models/addon.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as application from '@clevercloud/client/esm/api/v2/application.js';
|
|
2
1
|
import cliparse from 'cliparse';
|
|
3
2
|
|
|
4
3
|
import { get as getAddon, getAll as getAllAddons, getAllEnvVars, remove as removeAddon, create as createAddon, update as updateAddon } from '@clevercloud/client/esm/api/v2/addon.js';
|
|
@@ -10,6 +9,7 @@ import * as Interact from './interact.js';
|
|
|
10
9
|
import { Logger } from '../logger.js';
|
|
11
10
|
import { sendToApi } from '../models/send-to-api.js';
|
|
12
11
|
import { resolveOwnerId } from './ids-resolver.js';
|
|
12
|
+
import { getAllLinkedAddons, linkAddon, unlinkAddon } from '@clevercloud/client/esm/api/v2/application.js';
|
|
13
13
|
|
|
14
14
|
export function listProviders () {
|
|
15
15
|
return getAllAddonProviders({}).then(sendToApi);
|
|
@@ -42,7 +42,7 @@ export async function list (ownerId, appId, showAll) {
|
|
|
42
42
|
return allAddons;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const myAddons = await
|
|
45
|
+
const myAddons = await getAllLinkedAddons({ id: ownerId, appId }).then(sendToApi);
|
|
46
46
|
|
|
47
47
|
if (showAll == null) {
|
|
48
48
|
return myAddons;
|
|
@@ -191,12 +191,12 @@ async function getId (ownerId, addon) {
|
|
|
191
191
|
|
|
192
192
|
export async function link (ownerId, appId, addon) {
|
|
193
193
|
const addonId = await getId(ownerId, addon);
|
|
194
|
-
return
|
|
194
|
+
return linkAddon({ id: ownerId, appId }, JSON.stringify(addonId)).then(sendToApi);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
export async function unlink (ownerId, appId, addon) {
|
|
198
198
|
const addonId = await getId(ownerId, addon);
|
|
199
|
-
return
|
|
199
|
+
return unlinkAddon({ id: ownerId, appId, addonId }).then(sendToApi);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
export async function deleteAddon (ownerId, addonIdOrName, skipConfirmation) {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
create as createApplication,
|
|
4
|
+
remove as removeApplication,
|
|
5
|
+
getAll as getAllApplications,
|
|
6
|
+
get as getApplication,
|
|
7
|
+
redeploy as redeployApplication,
|
|
8
|
+
update as updateApplication,
|
|
9
|
+
getAllDependencies,
|
|
10
|
+
addDependency,
|
|
11
|
+
removeDependency,
|
|
12
|
+
} from '@clevercloud/client/esm/api/v2/application.js';
|
|
3
13
|
import cliparse from 'cliparse';
|
|
4
|
-
import * as product from '@clevercloud/client/esm/api/v2/product.js';
|
|
5
14
|
import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
|
|
6
15
|
|
|
7
16
|
import * as AppConfiguration from './app_configuration.js';
|
|
@@ -12,6 +21,7 @@ import * as User from './user.js';
|
|
|
12
21
|
|
|
13
22
|
import { sendToApi } from '../models/send-to-api.js';
|
|
14
23
|
import { resolveOwnerId } from './ids-resolver.js';
|
|
24
|
+
import { getAvailableInstances } from '@clevercloud/client/esm/api/v2/product.js';
|
|
15
25
|
|
|
16
26
|
export function listAvailableTypes () {
|
|
17
27
|
return cliparse.autocomplete.words(['docker', 'elixir', 'go', 'gradle', 'haskell', 'jar', 'maven', 'meteor', 'node', 'php', 'play1', 'play2', 'python', 'ruby', 'rust', 'sbt', 'static-apache', 'war']);
|
|
@@ -42,7 +52,7 @@ async function getId (ownerId, dependency) {
|
|
|
42
52
|
async function getInstanceType (type) {
|
|
43
53
|
|
|
44
54
|
// TODO: We should be able to use it without {}
|
|
45
|
-
const types = await
|
|
55
|
+
const types = await getAvailableInstances({}).then(sendToApi);
|
|
46
56
|
|
|
47
57
|
const enabledTypes = types.filter((t) => t.enabled);
|
|
48
58
|
const matchingVariants = enabledTypes.filter((t) => t.variant != null && t.variant.slug === type);
|
|
@@ -83,7 +93,7 @@ export async function create (name, typeName, region, orgaIdOrName, github, isTa
|
|
|
83
93
|
newApp.oauthApp = github;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
return
|
|
96
|
+
return createApplication({ id: ownerId }, newApp).then(sendToApi);
|
|
87
97
|
};
|
|
88
98
|
|
|
89
99
|
export async function deleteApp (app, skipConfirmation) {
|
|
@@ -97,7 +107,7 @@ export async function deleteApp (app, skipConfirmation) {
|
|
|
97
107
|
);
|
|
98
108
|
}
|
|
99
109
|
|
|
100
|
-
return
|
|
110
|
+
return removeApplication({ id: app.ownerId, appId: app.id }).then(sendToApi);
|
|
101
111
|
};
|
|
102
112
|
|
|
103
113
|
export async function getAllApps (ownerId) {
|
|
@@ -123,7 +133,7 @@ export async function getAllApps (ownerId) {
|
|
|
123
133
|
};
|
|
124
134
|
|
|
125
135
|
async function getApplicationsForOwner (ownerId) {
|
|
126
|
-
const rawApplications = await
|
|
136
|
+
const rawApplications = await getAllApplications({ id: ownerId }).then(sendToApi);
|
|
127
137
|
return rawApplications.map((app) => {
|
|
128
138
|
return {
|
|
129
139
|
app_id: app.id,
|
|
@@ -150,13 +160,13 @@ function getApplicationByName (apps, name) {
|
|
|
150
160
|
};
|
|
151
161
|
|
|
152
162
|
async function getByName (ownerId, name) {
|
|
153
|
-
const apps = await
|
|
163
|
+
const apps = await getAllApplications({ id: ownerId }).then(sendToApi);
|
|
154
164
|
return getApplicationByName(apps, name);
|
|
155
165
|
};
|
|
156
166
|
|
|
157
167
|
export function get (ownerId, appId) {
|
|
158
168
|
Logger.debug(`Get information for the app: ${appId}`);
|
|
159
|
-
return
|
|
169
|
+
return getApplication({ id: ownerId, appId }).then(sendToApi);
|
|
160
170
|
};
|
|
161
171
|
|
|
162
172
|
function getFromSelf (appId) {
|
|
@@ -164,7 +174,7 @@ function getFromSelf (appId) {
|
|
|
164
174
|
// /self differs from /organisations only for this one:
|
|
165
175
|
// it fallbacks to the organisations of which the user
|
|
166
176
|
// is a member, if it doesn't belong to Personal Space.
|
|
167
|
-
return
|
|
177
|
+
return getApplication({ appId }).then(sendToApi);
|
|
168
178
|
};
|
|
169
179
|
|
|
170
180
|
/**
|
|
@@ -245,7 +255,7 @@ export function unlinkRepo (alias) {
|
|
|
245
255
|
export function redeploy (ownerId, appId, commit, withoutCache) {
|
|
246
256
|
Logger.debug(`Redeploying the app: ${appId}`);
|
|
247
257
|
const useCache = (withoutCache) ? 'no' : null;
|
|
248
|
-
return
|
|
258
|
+
return redeployApplication({ id: ownerId, appId, commit, useCache }).then(sendToApi);
|
|
249
259
|
};
|
|
250
260
|
|
|
251
261
|
export function mergeScalabilityParameters (scalabilityParameters, instance) {
|
|
@@ -283,7 +293,7 @@ export function mergeScalabilityParameters (scalabilityParameters, instance) {
|
|
|
283
293
|
export async function setScalability (appId, ownerId, scalabilityParameters, buildFlavor) {
|
|
284
294
|
Logger.info('Scaling the app: ' + appId);
|
|
285
295
|
|
|
286
|
-
const app = await
|
|
296
|
+
const app = await getApplication({ id: ownerId, appId }).then(sendToApi);
|
|
287
297
|
const instance = _.cloneDeep(app.instance);
|
|
288
298
|
|
|
289
299
|
instance.minFlavor = instance.minFlavor.name;
|
|
@@ -301,17 +311,17 @@ export async function setScalability (appId, ownerId, scalabilityParameters, bui
|
|
|
301
311
|
}
|
|
302
312
|
}
|
|
303
313
|
|
|
304
|
-
return
|
|
314
|
+
return updateApplication({ id: ownerId, appId }, newConfig).then(sendToApi);
|
|
305
315
|
};
|
|
306
316
|
|
|
307
317
|
export async function listDependencies (ownerId, appId, showAll) {
|
|
308
|
-
const applicationDeps = await
|
|
318
|
+
const applicationDeps = await getAllDependencies({ id: ownerId, appId }).then(sendToApi);
|
|
309
319
|
|
|
310
320
|
if (!showAll) {
|
|
311
321
|
return applicationDeps.map((app) => ({ ...app, isLinked: true }));
|
|
312
322
|
}
|
|
313
323
|
|
|
314
|
-
const allApps = await
|
|
324
|
+
const allApps = await getAllApplications({ id: ownerId }).then(sendToApi);
|
|
315
325
|
|
|
316
326
|
const applicationDepsIds = applicationDeps.map((app) => app.id);
|
|
317
327
|
return allApps.map((app) => {
|
|
@@ -322,10 +332,10 @@ export async function listDependencies (ownerId, appId, showAll) {
|
|
|
322
332
|
|
|
323
333
|
export async function link (ownerId, appId, dependency) {
|
|
324
334
|
const dependencyId = await getId(ownerId, dependency);
|
|
325
|
-
return
|
|
335
|
+
return addDependency({ id: ownerId, appId, dependencyId }).then(sendToApi);
|
|
326
336
|
};
|
|
327
337
|
|
|
328
338
|
export async function unlink (ownerId, appId, dependency) {
|
|
329
339
|
const dependencyId = await getId(ownerId, dependency);
|
|
330
|
-
return
|
|
340
|
+
return removeDependency({ id: ownerId, appId, dependencyId }).then(sendToApi);
|
|
331
341
|
};
|
|
@@ -123,6 +123,7 @@ export async function setFeature (feature, value) {
|
|
|
123
123
|
|
|
124
124
|
export const conf = env.getOrElseAll({
|
|
125
125
|
API_HOST: 'https://api.clever-cloud.com',
|
|
126
|
+
AUTH_BRIDGE_HOST: 'https://api-bridge.clever-cloud.com',
|
|
126
127
|
SSH_GATEWAY: 'ssh@sshgateway-clevercloud-customers.services.clever-cloud.com',
|
|
127
128
|
|
|
128
129
|
// the disclosure of these tokens is not considered as a vulnerability. Do not report this to our security service.
|
package/src/models/namespaces.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as Application from './application.js';
|
|
2
|
-
import
|
|
2
|
+
import { getNamespaces as getTcpRedirNamespaces } from '@clevercloud/client/esm/api/v2/organisation.js';
|
|
3
3
|
import { sendToApi } from './send-to-api.js';
|
|
4
4
|
import cliparse from 'cliparse';
|
|
5
|
+
|
|
5
6
|
export async function getNamespaces (ownerId) {
|
|
6
|
-
return
|
|
7
|
+
return getTcpRedirNamespaces({ id: ownerId }).then(sendToApi);
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export async function completeNamespaces () {
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import colors from 'colors/safe.js';
|
|
2
|
+
import * as networkGroup from './ng.js';
|
|
3
|
+
import * as networkGroupApi from '@clevercloud/client/esm/api/v4/network-group.js';
|
|
4
|
+
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import { setTimeout } from 'node:timers/promises';
|
|
7
|
+
import { Logger } from '../logger.js';
|
|
8
|
+
import { sendToApi } from './send-to-api.js';
|
|
9
|
+
import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create an external peer and link its parent member to the Network Group
|
|
13
|
+
* @param {object} ngIdOrLabel The Network Group ID or Label
|
|
14
|
+
* @param {string} peerLabel External peer label
|
|
15
|
+
* @param {string} publicKey External peer public key
|
|
16
|
+
* @param {object} org Organisation ID or name
|
|
17
|
+
* @throws {Error} If a valid peer label is not provided
|
|
18
|
+
* @throws {Error} If the Network Group is not found
|
|
19
|
+
* @throws {Error} If the parent member is not linked to the Network Group
|
|
20
|
+
* @throws {Error} If the external peer is not linked to the Network Group
|
|
21
|
+
*/
|
|
22
|
+
export async function createExternalPeerWithParent (ngIdOrLabel, peerLabel, publicKey, org) {
|
|
23
|
+
|
|
24
|
+
if (!peerLabel) {
|
|
25
|
+
throw new Error('A valid peer label is required');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
|
|
29
|
+
|
|
30
|
+
if (!ng) {
|
|
31
|
+
throw new Error(`Network Group ${colors.red(ngIdOrLabel.ngId || ngIdOrLabel.ngResourceLabel)} not found`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// We define a parent member for the external peer
|
|
35
|
+
const id = `external_${crypto.randomUUID()}`;
|
|
36
|
+
const parentMember = {
|
|
37
|
+
id,
|
|
38
|
+
label: `Parent of ${peerLabel}`,
|
|
39
|
+
domainName: `${id}.m.${ng.id}.${networkGroup.DOMAIN}`,
|
|
40
|
+
kind: 'EXTERNAL',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
Logger.info(`Creating a parent member ${parentMember.id} linked to Network Group ${ng.id}`);
|
|
44
|
+
await linkMember({ ngId: ng.id }, parentMember.id, org, parentMember.label);
|
|
45
|
+
|
|
46
|
+
const checkParentMember = await checkResource(ng.id, org, parentMember.id, true);
|
|
47
|
+
if (!checkParentMember) {
|
|
48
|
+
throw new Error(`Parent member ${colors.red(parentMember.id)} not linked to Network Group ${colors.red(ng.id)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Logger.info(`Parent member ${parentMember.id} created and linked to Network Group ${ng.id}`);
|
|
52
|
+
|
|
53
|
+
// We define the external peer, for now we only support client role
|
|
54
|
+
const body = {
|
|
55
|
+
peerRole: 'CLIENT',
|
|
56
|
+
publicKey,
|
|
57
|
+
label: peerLabel,
|
|
58
|
+
parentMember: parentMember.id,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
Logger.info(`Adding external peer to Member ${parentMember.id} of Network Group ${ng.id}`);
|
|
62
|
+
Logger.debug('Sending body: ' + JSON.stringify(body, null, 2));
|
|
63
|
+
await networkGroupApi.createNetworkGroupExternalPeer({ ownerId: ng.ownerId, networkGroupId: ng.id }, body).then(sendToApi);
|
|
64
|
+
|
|
65
|
+
const checkExternalPeer = await checkResource(ng.id, org, peerLabel, true, 'peer', 'label');
|
|
66
|
+
if (!checkExternalPeer) {
|
|
67
|
+
throw new Error(`External peer ${colors.red(peerLabel)} not linked to Network Group ${colors.red(ng.id)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Logger.info(`External peer ${peerLabel} added to Member ${parentMember.id} of Network Group ${ng.id}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Delete an external peer and its parent member from a Network Group
|
|
75
|
+
* @param {object} ngIdOrLabel Network Group ID or label
|
|
76
|
+
* @param {string} peerIdOrLabel External peer ID or label
|
|
77
|
+
* @param {object} org Organisation ID or name
|
|
78
|
+
* @throws {Error} If the Network Group is not found
|
|
79
|
+
* @throws {Error} If the External Peer is not found
|
|
80
|
+
* @throws {Error} If the External Peer is still linked to the Network Group
|
|
81
|
+
* @throws {Error} If the Parent Member is still linked to the Network Group
|
|
82
|
+
*/
|
|
83
|
+
export async function deleteExternalPeerWithParent (ngIdOrLabel, peerIdOrLabel, org) {
|
|
84
|
+
|
|
85
|
+
const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
|
|
86
|
+
|
|
87
|
+
if (!ng) {
|
|
88
|
+
throw new Error(`Network Group ${colors.red(ngIdOrLabel.ngId || ngIdOrLabel.ngResourceLabel)} not found`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const externalPeer = ng.peers.find((p) => {
|
|
92
|
+
return p.id === peerIdOrLabel || p.label === peerIdOrLabel;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!externalPeer) {
|
|
96
|
+
throw new Error(`External peer ${colors.red(peerIdOrLabel)} not found`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Logger.info(`Deleting external peer ${externalPeer.id} from Network Group ${ng.id}`);
|
|
100
|
+
await networkGroupApi.deleteNetworkGroupExternalPeer({ ownerId: ng.ownerId, networkGroupId: ng.id, peerId: externalPeer.id }).then(sendToApi);
|
|
101
|
+
|
|
102
|
+
const checkPeer = await checkResource(ng.id, org, externalPeer.id, false, 'peer');
|
|
103
|
+
if (!checkPeer) {
|
|
104
|
+
throw new Error(`External peer ${colors.red(externalPeer.id)} still linked to Network Group ${colors.red(ng.id)}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Logger.info(`External peer ${externalPeer.id} deleted from Network Group ${ng.id}`);
|
|
108
|
+
Logger.info(`Unlinking parent member ${externalPeer.parentMember} from Network Group ${ng.id}`);
|
|
109
|
+
|
|
110
|
+
await unlinkMember(ngIdOrLabel, externalPeer.parentMember, org);
|
|
111
|
+
|
|
112
|
+
const checkParentMember = await checkResource(ng.id, org, externalPeer.parentMember, false);
|
|
113
|
+
if (!checkParentMember) {
|
|
114
|
+
throw new Error(`Parent member ${colors.red(externalPeer.parentMember)} still linked to Network Group ${colors.red(ng.id)}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Logger.info(`Parent member ${externalPeer.parentMember} unlinked from Network Group ${ng.id}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Link a Member to a Network Group
|
|
122
|
+
* @param {object} ngIdOrLabel The Network group ID or Label
|
|
123
|
+
* @param {string} memberId ID of the Member to link
|
|
124
|
+
* @param {object} org Organisation ID or name
|
|
125
|
+
* @param {string} label Label of the Member
|
|
126
|
+
*/
|
|
127
|
+
export async function linkMember (ngIdOrLabel, memberId, org, label) {
|
|
128
|
+
if (!memberId) {
|
|
129
|
+
throw new Error('A valid member ID is required (addon_xxx, app_xxx, external_xxx)');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
|
|
133
|
+
|
|
134
|
+
if (!ng) {
|
|
135
|
+
throw new Error(`Network Group ${colors.red(ngIdOrLabel.ngId || ngIdOrLabel.ngResourceLabel)} not found`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await checkMembersToLink([memberId], ng.ownerId);
|
|
139
|
+
|
|
140
|
+
const alreadyMember = ng.members.find((m) => m.id === memberId);
|
|
141
|
+
if (alreadyMember) {
|
|
142
|
+
throw new Error(`Member ${colors.red(memberId)} is already linked to Network Group ${colors.red(ng.id)}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const [member] = networkGroup.constructMembers(ng.id, [memberId]);
|
|
146
|
+
|
|
147
|
+
const body = {
|
|
148
|
+
id: member.id,
|
|
149
|
+
label: label || member.label,
|
|
150
|
+
domainName: member.domainName,
|
|
151
|
+
kind: member.kind,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
Logger.info(`Linking member ${member.id} to Network Group ${ng.id}`);
|
|
155
|
+
Logger.debug('Sending body: ' + JSON.stringify(body, null, 2));
|
|
156
|
+
await networkGroupApi.createNetworkGroupMember({ ownerId: ng.ownerId, networkGroupId: ng.id }, body).then(sendToApi);
|
|
157
|
+
|
|
158
|
+
const check = await checkResource(ng.id, org, member.id, true);
|
|
159
|
+
if (!check) {
|
|
160
|
+
throw new Error(`Member ${colors.red(member.id)} not linked to Network Group ${colors.red(ng.id)}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
Logger.info(`Member ${member.id} linked to Network Group ${ng.id}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Unlink a Member from a Network Group
|
|
168
|
+
* @param {object} ngIdOrLabel The Network Group ID or Label
|
|
169
|
+
* @param {string} memberId The Member ID
|
|
170
|
+
* @param {object} org Organisation ID or name
|
|
171
|
+
* @throws {Error} If a valid member ID is not provided
|
|
172
|
+
* @throws {Error} If the Network Group is not found
|
|
173
|
+
* @throws {Error} If the Member is not found in the Network Group
|
|
174
|
+
* @throws {Error} If the Member is still linked to the Network Group
|
|
175
|
+
*/
|
|
176
|
+
export async function unlinkMember (ngIdOrLabel, memberId, org) {
|
|
177
|
+
if (!memberId) {
|
|
178
|
+
throw new Error('A valid member ID is required (addon_xxx, app_xxx, external_xxx)');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
|
|
182
|
+
|
|
183
|
+
if (!ng) {
|
|
184
|
+
throw new Error(`Network Group ${colors.red(ngIdOrLabel.ngId || ngIdOrLabel.ngLabel)} not found`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const member = ng.members.find((m) => m.id === memberId);
|
|
188
|
+
if (!member) {
|
|
189
|
+
throw new Error(`Member ${colors.red(memberId)} not found in Network Group ${colors.red(ng.id)}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
Logger.info(`Unlinking member ${memberId} from Network Group ${ng.id}`);
|
|
193
|
+
await networkGroupApi.deleteNetworkGroupMember({ ownerId: ng.ownerId, networkGroupId: ng.id, memberId }).then(sendToApi);
|
|
194
|
+
|
|
195
|
+
const check = await checkResource(ng.id, org, memberId, false);
|
|
196
|
+
if (!check) {
|
|
197
|
+
throw new Error(`Member ${colors.red(memberId)} still linked to Network Group ${colors.red(ng.id)}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
Logger.info(`Member ${memberId} unlinked from Network Group ${ng.id}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if members can be linked to a Network Group
|
|
205
|
+
* @param {Array<string>} members Members to check
|
|
206
|
+
* @throws {Error} If members can't be linked to a Network Group
|
|
207
|
+
*/
|
|
208
|
+
export async function checkMembersToLink (members, ownerId) {
|
|
209
|
+
const VALID_ADDON_PROVIDERS = [
|
|
210
|
+
'es-addon',
|
|
211
|
+
'mongodb-addon',
|
|
212
|
+
'mysql-addon',
|
|
213
|
+
'postgresql-addon',
|
|
214
|
+
'redis-addon',
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const summary = await getSummary().then(sendToApi);
|
|
218
|
+
|
|
219
|
+
let data = summary.user;
|
|
220
|
+
if (summary.user.id !== ownerId) {
|
|
221
|
+
data = summary.organisations.find((o) => o.id === ownerId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const membersNotOK = [];
|
|
225
|
+
let source = data.applications;
|
|
226
|
+
|
|
227
|
+
for (const memberId of members) {
|
|
228
|
+
if (memberId.startsWith('addon_')) source = data.addons;
|
|
229
|
+
|
|
230
|
+
const foundRessource = source.find((r) => r.id === memberId);
|
|
231
|
+
|
|
232
|
+
if (foundRessource && memberId.startsWith('addon_') && !VALID_ADDON_PROVIDERS.includes(foundRessource.providerId)) {
|
|
233
|
+
membersNotOK.push(memberId);
|
|
234
|
+
}
|
|
235
|
+
else if (!foundRessource && !memberId.startsWith('external_')) {
|
|
236
|
+
membersNotOK.push(memberId);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (membersNotOK.length > 0) {
|
|
241
|
+
throw new Error(`Member(s) ${colors.red(membersNotOK.join(', '))} can't be linked to the Network Group, check Organisation ID or name`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if a resource is present in a Network Group by ID or label
|
|
247
|
+
* @param {string} ngId Network Group ID
|
|
248
|
+
* @param {object} org Organisation ID or name
|
|
249
|
+
* @param {string} resource Resource ID or label
|
|
250
|
+
* @param {boolean} shouldBePresent Expected presence of the resource
|
|
251
|
+
* @param {string} [resourceType] Resource type (member or peer), default is member
|
|
252
|
+
* @param {string} [searchBy] Search by 'id' or 'label', default is 'id'
|
|
253
|
+
* @returns {Promise<boolean>} True if the resource is present, false otherwise
|
|
254
|
+
*/
|
|
255
|
+
async function checkResource (ngId, org, resource, shouldBePresent, resourceType = 'member', searchBy = 'id') {
|
|
256
|
+
const endTime = Date.now() + networkGroup.POLLING_TIMEOUT_MS;
|
|
257
|
+
|
|
258
|
+
while (Date.now() < endTime) {
|
|
259
|
+
const ng = await networkGroup.getNG(ngId, org);
|
|
260
|
+
const items = resourceType === 'member' ? ng.members : ng.peers;
|
|
261
|
+
const isPresent = items.some((item) => item[searchBy] === resource);
|
|
262
|
+
|
|
263
|
+
if (isPresent === shouldBePresent) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await setTimeout(networkGroup.POLLING_INTERVAL_MS);
|
|
268
|
+
}
|
|
269
|
+
return false;
|
|
270
|
+
}
|