clever-tools 3.11.0 → 3.13.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 +416 -3
- package/package.json +4 -3
- package/src/clever-client/auth-bridge.js +61 -0
- package/src/clever-client/ng.js +18 -0
- package/src/clever-client/operators.js +121 -0
- package/src/commands/addon.js +34 -28
- package/src/commands/cancel-deploy.js +3 -4
- package/src/commands/config.js +16 -20
- package/src/commands/console.js +4 -10
- package/src/commands/create.js +76 -9
- package/src/commands/curl.js +17 -17
- package/src/commands/database.js +9 -9
- package/src/commands/delete.js +11 -10
- package/src/commands/deploy.js +35 -21
- package/src/commands/domain.js +13 -16
- package/src/commands/emails.js +174 -0
- package/src/commands/env.js +15 -8
- package/src/commands/keycloak.js +138 -0
- package/src/commands/kv.js +1 -1
- package/src/commands/link.js +7 -3
- package/src/commands/makeDefault.js +2 -1
- package/src/commands/matomo.js +85 -0
- package/src/commands/metabase.js +112 -0
- package/src/commands/ng.js +202 -0
- package/src/commands/open.js +2 -5
- package/src/commands/otoroshi.js +138 -0
- package/src/commands/profile.js +11 -10
- package/src/commands/published-config.js +7 -7
- package/src/commands/restart.js +1 -1
- package/src/commands/ssh-keys.js +159 -0
- package/src/commands/stop.js +3 -3
- package/src/commands/tcp-redirs.js +8 -8
- package/src/commands/tokens.js +146 -0
- package/src/commands/unlink.js +2 -1
- package/src/experimental-features.js +47 -2
- package/src/lib/ng-print.js +195 -0
- package/src/lib/operator-commands.js +281 -0
- package/src/lib/prompts.js +30 -0
- package/src/lib/slugify.js +10 -0
- package/src/logger.js +3 -0
- package/src/models/activity.js +2 -3
- package/src/models/addon.js +9 -6
- package/src/models/app_configuration.js +12 -9
- package/src/models/application.js +44 -22
- package/src/models/application_configuration.js +72 -72
- package/src/models/configuration.js +1 -0
- package/src/models/git.js +29 -1
- package/src/models/ids-resolver.js +37 -1
- package/src/models/interact.js +0 -24
- package/src/models/log-v4.js +13 -4
- 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/operator.js +48 -0
- package/src/models/send-to-api.js +18 -0
- package/src/models/utils.js +21 -0
- package/src/parsers.js +71 -3
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import cliparse from 'cliparse';
|
|
2
|
-
import colors from 'colors/safe.js';
|
|
3
|
-
|
|
4
2
|
import { Logger } from '../logger.js';
|
|
3
|
+
import dedent from 'dedent';
|
|
5
4
|
|
|
6
5
|
const CONFIG_KEYS = [
|
|
7
6
|
{ id: 'name', name: 'name', displayName: 'Name', kind: 'string' },
|
|
@@ -10,31 +9,43 @@ const CONFIG_KEYS = [
|
|
|
10
9
|
{ id: 'sticky-sessions', name: 'stickySessions', displayName: 'Sticky sessions', kind: 'bool' },
|
|
11
10
|
{ id: 'cancel-on-push', name: 'cancelOnPush', displayName: 'Cancel current deployment on push', kind: 'bool' },
|
|
12
11
|
{ id: 'force-https', name: 'forceHttps', displayName: 'Force redirection of HTTP to HTTPS', kind: 'force-https' },
|
|
12
|
+
{ id: 'task', name: 'instanceLifetime', displayName: 'Deploy an application as a Clever Task', kind: 'task' },
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
-
export function listAvailableIds () {
|
|
16
|
-
|
|
15
|
+
export function listAvailableIds (asText = false) {
|
|
16
|
+
const ids = CONFIG_KEYS.map((config) => config.id);
|
|
17
|
+
if (asText) {
|
|
18
|
+
return new Intl
|
|
19
|
+
.ListFormat('en', { style: 'short', type: 'disjunction' })
|
|
20
|
+
.format(ids);
|
|
21
|
+
}
|
|
22
|
+
return ids;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
export function getById (id) {
|
|
20
26
|
const config = CONFIG_KEYS.find((config) => config.id === id);
|
|
21
|
-
if (config
|
|
22
|
-
|
|
23
|
-
Logger.error(`Available configuration names are: ${listAvailableIds().join(', ')}.`);
|
|
27
|
+
if (config != null) {
|
|
28
|
+
return config;
|
|
24
29
|
}
|
|
25
|
-
|
|
30
|
+
throw new Error(dedent`
|
|
31
|
+
Invalid configuration name: ${id}.
|
|
32
|
+
Available configuration names: ${listAvailableIds(true)}.
|
|
33
|
+
`);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
function
|
|
36
|
+
export function formatValue (config, value) {
|
|
29
37
|
switch (config.kind) {
|
|
30
38
|
case 'bool': {
|
|
31
|
-
return
|
|
39
|
+
return value;
|
|
32
40
|
}
|
|
33
41
|
case 'inverted-bool': {
|
|
34
|
-
return
|
|
42
|
+
return !value;
|
|
35
43
|
}
|
|
36
44
|
case 'force-https': {
|
|
37
|
-
return value
|
|
45
|
+
return value === 'ENABLED';
|
|
46
|
+
}
|
|
47
|
+
case 'task': {
|
|
48
|
+
return value === 'TASK';
|
|
38
49
|
}
|
|
39
50
|
default: {
|
|
40
51
|
return String(value);
|
|
@@ -44,14 +55,26 @@ function display (config, value) {
|
|
|
44
55
|
|
|
45
56
|
export function parse (config, value) {
|
|
46
57
|
switch (config.kind) {
|
|
47
|
-
case 'bool':
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
case '
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
case 'bool':
|
|
59
|
+
case 'inverted-bool':
|
|
60
|
+
case 'force-https':
|
|
61
|
+
case 'task': {
|
|
62
|
+
if (value !== 'true' && value !== 'false') {
|
|
63
|
+
throw new Error('Invalid configuration value, it must be a boolean (true or false)');
|
|
64
|
+
}
|
|
65
|
+
if (config.kind === 'bool') {
|
|
66
|
+
return (value === 'true');
|
|
67
|
+
}
|
|
68
|
+
if (config.kind === 'inverted-bool') {
|
|
69
|
+
return (value === 'false');
|
|
70
|
+
}
|
|
71
|
+
if (config.kind === 'force-https') {
|
|
72
|
+
return (value === 'true') ? 'ENABLED' : 'DISABLED';
|
|
73
|
+
}
|
|
74
|
+
if (config.kind === 'task') {
|
|
75
|
+
return (value === 'false') ? 'REGULAR' : 'TASK';
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
55
78
|
}
|
|
56
79
|
default: {
|
|
57
80
|
return value;
|
|
@@ -69,7 +92,8 @@ function getConfigOptions (config) {
|
|
|
69
92
|
switch (config.kind) {
|
|
70
93
|
case 'bool':
|
|
71
94
|
case 'inverted-bool':
|
|
72
|
-
case 'force-https':
|
|
95
|
+
case 'force-https':
|
|
96
|
+
case 'task': {
|
|
73
97
|
return [
|
|
74
98
|
cliparse.flag(`enable-${config.id}`, { description: `Enable ${config.id}` }),
|
|
75
99
|
cliparse.flag(`disable-${config.id}`, { description: `Disable ${config.id}` }),
|
|
@@ -92,69 +116,45 @@ export function parseOptions (options) {
|
|
|
92
116
|
|
|
93
117
|
function parseConfigOption (config, options) {
|
|
94
118
|
switch (config.kind) {
|
|
95
|
-
case 'bool':
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Logger.warn(`${config.id} is both enabled and disabled, ignoring`);
|
|
100
|
-
}
|
|
101
|
-
else if (enable || disable) {
|
|
102
|
-
return [config.name, enable];
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
case 'inverted-bool': {
|
|
107
|
-
const disable = options[`enable-${config.id}`];
|
|
108
|
-
const enable = options[`disable-${config.id}`];
|
|
109
|
-
if (enable && disable) {
|
|
110
|
-
Logger.warn(`${config.id} is both enabled and disabled, ignoring`);
|
|
111
|
-
}
|
|
112
|
-
else if (enable || disable) {
|
|
113
|
-
return [config.name, enable];
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
case 'force-https': {
|
|
119
|
+
case 'bool':
|
|
120
|
+
case 'inverted-bool':
|
|
121
|
+
case 'force-https':
|
|
122
|
+
case 'task': {
|
|
118
123
|
const enable = options[`enable-${config.id}`];
|
|
119
124
|
const disable = options[`disable-${config.id}`];
|
|
120
125
|
if (enable && disable) {
|
|
121
|
-
|
|
126
|
+
throw new Error(`You cannot use both --enable-${config.id} and --disable-${config.id} at the same time`);
|
|
122
127
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
if (enable || disable) {
|
|
129
|
+
if (config.kind === 'bool') {
|
|
130
|
+
return [config.name, enable];
|
|
131
|
+
}
|
|
132
|
+
if (config.kind === 'inverted-bool') {
|
|
133
|
+
return [config.name, disable];
|
|
134
|
+
}
|
|
135
|
+
if (config.kind === 'force-https' || config.kind === 'task') {
|
|
136
|
+
return [config.name, parse(config, String(enable))];
|
|
137
|
+
}
|
|
126
138
|
}
|
|
127
|
-
return
|
|
139
|
+
return;
|
|
128
140
|
}
|
|
129
141
|
default: {
|
|
130
|
-
|
|
131
|
-
return [config.name, options[config.id]];
|
|
132
|
-
}
|
|
133
|
-
return null;
|
|
142
|
+
return [config.name, options[config.id]];
|
|
134
143
|
}
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
146
|
|
|
138
|
-
function
|
|
139
|
-
if (app[config.name] != null) {
|
|
140
|
-
Logger.println(`${config.displayName}: ${colors.bold(display(config, app[config.name]))}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function printById (app, id) {
|
|
147
|
+
export function printValue (app, id) {
|
|
145
148
|
const config = getById(id);
|
|
146
|
-
|
|
147
|
-
printConfig(app, config);
|
|
148
|
-
}
|
|
149
|
+
Logger.println(formatValue(config, app[config.name]));
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
export function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
152
|
+
export function printAllValues (app) {
|
|
153
|
+
console.table(
|
|
154
|
+
Object.fromEntries(
|
|
155
|
+
CONFIG_KEYS.map((config) => {
|
|
156
|
+
return [config.id, formatValue(config, app[config.name])];
|
|
157
|
+
}),
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
160
|
}
|
|
@@ -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/git.js
CHANGED
|
@@ -5,7 +5,7 @@ import _ from 'lodash';
|
|
|
5
5
|
import git from 'isomorphic-git';
|
|
6
6
|
import * as http from './isomorphic-http-with-agent.js';
|
|
7
7
|
import cliparse from 'cliparse';
|
|
8
|
-
import slugify from 'slugify';
|
|
8
|
+
import { slugify } from '../lib/slugify.js';
|
|
9
9
|
import { findPath } from './fs-utils.js';
|
|
10
10
|
import { loadOAuthConf } from './configuration.js';
|
|
11
11
|
|
|
@@ -130,3 +130,31 @@ export async function isShallow () {
|
|
|
130
130
|
return false;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if the current directory is a git repository
|
|
136
|
+
* @returns {Promise<boolean>}
|
|
137
|
+
*/
|
|
138
|
+
export async function isInsideGitRepo () {
|
|
139
|
+
return getRepo()
|
|
140
|
+
.then(() => true)
|
|
141
|
+
.catch(() => false);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if the current git working directory is clean
|
|
146
|
+
* @returns {Promise<boolean>}
|
|
147
|
+
*/
|
|
148
|
+
export async function isGitWorkingDirectoryClean () {
|
|
149
|
+
const repo = await getRepo();
|
|
150
|
+
const status = await git.statusMatrix({ ...repo });
|
|
151
|
+
const isStatusEmpty = status
|
|
152
|
+
.filter(([filepath, head, workdir]) => {
|
|
153
|
+
// WARNING: isomorphic-git does not support global gitignore so we filter hidden files and dirs to reduce the amount of false positives
|
|
154
|
+
const isHidden = filepath.startsWith('.');
|
|
155
|
+
const isCleverJson = filepath === '.clever.json';
|
|
156
|
+
return (!isHidden || isCleverJson) && head !== workdir;
|
|
157
|
+
})
|
|
158
|
+
.length === 0;
|
|
159
|
+
return isStatusEmpty;
|
|
160
|
+
}
|
|
@@ -110,7 +110,7 @@ async function getIdsFromSummary () {
|
|
|
110
110
|
* @param {{ orga_name?: string, orga_id?: string }} ownerNameOrId
|
|
111
111
|
* @throws {Error} if no add-on is found
|
|
112
112
|
* @throws {Error} if several add-ons are found
|
|
113
|
-
* @returns {Object} The
|
|
113
|
+
* @returns {Object} The name, IDs and owner ID of the add-on { name, addonId, realId, ownerId }
|
|
114
114
|
*/
|
|
115
115
|
export async function findAddonsByNameOrId (addonIdOrRealIdOrName, ownerNameOrId) {
|
|
116
116
|
const summary = await getSummary().then(sendToApi);
|
|
@@ -128,7 +128,9 @@ export async function findAddonsByNameOrId (addonIdOrRealIdOrName, ownerNameOrId
|
|
|
128
128
|
return matchOwner && matchAddon;
|
|
129
129
|
})
|
|
130
130
|
.map(({ addon, owner }) => ({
|
|
131
|
+
name: addon.name,
|
|
131
132
|
addonId: addon.id,
|
|
133
|
+
realId: addon.realId,
|
|
132
134
|
ownerId: owner.id,
|
|
133
135
|
}));
|
|
134
136
|
|
|
@@ -139,3 +141,37 @@ export async function findAddonsByNameOrId (addonIdOrRealIdOrName, ownerNameOrId
|
|
|
139
141
|
|
|
140
142
|
return candidates;
|
|
141
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get the IDs and owners of found add-ons from a name, ID or real ID
|
|
147
|
+
* @param {string} addonIdOrRealIdOrName
|
|
148
|
+
* @throws {Error} if no add-on is found
|
|
149
|
+
* @throws {Error} if several add-ons are found
|
|
150
|
+
* @returns {Object} The name, IDs and owner ID of the add-on { name, addonId, realId, ownerId }
|
|
151
|
+
*/
|
|
152
|
+
export async function findAddonsByAddonProvider (provider) {
|
|
153
|
+
const summary = await getSummary().then(sendToApi);
|
|
154
|
+
|
|
155
|
+
Logger.debug(`Searching for ${provider} add-ons in ${summary.user.id} and ${summary.organisations.map((org) => org.id).join(', ')}`);
|
|
156
|
+
const candidates = [summary.user, ...summary.organisations]
|
|
157
|
+
.flatMap((owner) => {
|
|
158
|
+
return owner.addons
|
|
159
|
+
.filter((addon) => addon.providerId === provider)
|
|
160
|
+
.map((addon) => {
|
|
161
|
+
return {
|
|
162
|
+
name: addon.name,
|
|
163
|
+
addonId: addon.id,
|
|
164
|
+
realId: addon.realId,
|
|
165
|
+
ownerId: owner.id,
|
|
166
|
+
ownerName: owner.name,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
Logger.debug(`Found ${candidates.length} candidate(s) for provider ${provider}:`);
|
|
172
|
+
for (const candidate of candidates) {
|
|
173
|
+
Logger.debug(` - ${candidate.addonId} (${candidate.ownerId})`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return candidates;
|
|
177
|
+
}
|
package/src/models/interact.js
CHANGED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import readline from 'node:readline';
|
|
2
|
-
|
|
3
|
-
function ask (question) {
|
|
4
|
-
|
|
5
|
-
const rl = readline.createInterface({
|
|
6
|
-
input: process.stdin,
|
|
7
|
-
output: process.stdout,
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
rl.question(question, (answer) => {
|
|
12
|
-
rl.close();
|
|
13
|
-
resolve(answer);
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function confirm (question, rejectionMessage, expectedAnswers = ['yes', 'y']) {
|
|
19
|
-
const answer = await ask(question);
|
|
20
|
-
if (!expectedAnswers.includes(answer)) {
|
|
21
|
-
throw new Error(rejectionMessage);
|
|
22
|
-
}
|
|
23
|
-
return true;
|
|
24
|
-
}
|
package/src/models/log-v4.js
CHANGED
|
@@ -6,6 +6,8 @@ import { waitForDeploymentEnd, waitForDeploymentStart } from './deployments.js';
|
|
|
6
6
|
import { ApplicationLogStream } from '@clevercloud/client/esm/streams/application-logs.js';
|
|
7
7
|
import { JsonArray } from './json-array.js';
|
|
8
8
|
import * as ExitStrategy from '../models/exit-strategy-option.js';
|
|
9
|
+
import { getBest } from './domain.js';
|
|
10
|
+
import { conf } from './configuration.js';
|
|
9
11
|
|
|
10
12
|
// 2000 logs per 100ms maximum
|
|
11
13
|
const THROTTLE_ELEMENTS = 2000;
|
|
@@ -98,9 +100,11 @@ export async function watchDeploymentAndDisplayLogs (options) {
|
|
|
98
100
|
|
|
99
101
|
ExitStrategy.plotQuietWarning(exitStrategy, quiet);
|
|
100
102
|
// If in quiet mode, we only log start/finished deployment messages
|
|
101
|
-
!quiet
|
|
103
|
+
if (!quiet) {
|
|
104
|
+
Logger.println(` ${colors.blue('→ Waiting for deployment to start…')}`);
|
|
105
|
+
}
|
|
102
106
|
const deployment = await waitForDeploymentStart({ ownerId, appId, deploymentId, commitId, knownDeployments });
|
|
103
|
-
Logger.println(colors.
|
|
107
|
+
Logger.println(` ${colors.green(`✓ Deployment started ${colors.grey(`(${deployment.uuid})`)}`)}`);
|
|
104
108
|
|
|
105
109
|
if (exitStrategy === 'deploy-start') {
|
|
106
110
|
return;
|
|
@@ -119,7 +123,9 @@ export async function watchDeploymentAndDisplayLogs (options) {
|
|
|
119
123
|
logsStream = await displayLogs({ ownerId, appId, deploymentId: deployment.uuid, since: redeployDate, deferred });
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
!quiet
|
|
126
|
+
if (!quiet) {
|
|
127
|
+
Logger.println(` ${colors.blue('→ Waiting for application logs…')}`);
|
|
128
|
+
}
|
|
123
129
|
|
|
124
130
|
// Wait for deployment end (or an error thrown by logs with the deferred)
|
|
125
131
|
const deploymentEnded = await Promise.race([
|
|
@@ -132,7 +138,10 @@ export async function watchDeploymentAndDisplayLogs (options) {
|
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
if (deploymentEnded.state === 'OK') {
|
|
135
|
-
|
|
141
|
+
const favouriteDomain = await getBest(appId, ownerId);
|
|
142
|
+
Logger.println('');
|
|
143
|
+
Logger.println(`${colors.bold.green('✓ Access your application:')} ${colors.underline.bold(`https://${favouriteDomain.fqdn}`)}`);
|
|
144
|
+
Logger.println(`${colors.bold.blue('→ Manage your application:')} ${colors.underline.bold(`${conf.GOTO_URL}/${appId}`)}`);
|
|
136
145
|
}
|
|
137
146
|
else if (deploymentEnded.state === 'CANCELLED') {
|
|
138
147
|
throw new Error('Deployment was cancelled. Please check the activity');
|
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
|
+
}
|