clever-tools 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/clever.js +22 -20
- package/package.json +1 -1
- package/src/commands/addon.js +119 -146
- package/src/commands/diag.js +33 -4
- package/src/commands/ng.js +1 -1
- package/src/experimental-features.js +5 -5
- package/src/models/log-v4.js +1 -1
- package/src/models/ng-resources.js +16 -28
- package/src/models/ng.js +14 -15
- package/src/parsers.js +3 -2
package/bin/clever.js
CHANGED
|
@@ -79,6 +79,10 @@ process.stdout.on('error', (error) => {
|
|
|
79
79
|
const cliparseCommand = cliparse.command;
|
|
80
80
|
|
|
81
81
|
cliparse.command = function (name, options, commandFunction) {
|
|
82
|
+
if (commandFunction == null) {
|
|
83
|
+
return cliparseCommand(name, options);
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
return cliparseCommand(name, options, (...args) => {
|
|
83
87
|
const promise = commandFunction(...args);
|
|
84
88
|
handleCommandPromise(promise);
|
|
@@ -120,12 +124,16 @@ async function run() {
|
|
|
120
124
|
parser: Parsers.ngResourceType,
|
|
121
125
|
}),
|
|
122
126
|
ngAnyIdOrLabel: cliparse.argument('id-or-label', {
|
|
123
|
-
description: 'ID or Label of a Network
|
|
127
|
+
description: 'ID or Label of a Network Group, a member or an (external) peer',
|
|
128
|
+
parser: Parsers.ngResourceType,
|
|
129
|
+
}),
|
|
130
|
+
ngResourceId: cliparse.argument('id', {
|
|
131
|
+
description: 'ID of a resource to (un)link to a Network Group',
|
|
124
132
|
parser: Parsers.ngResourceType,
|
|
125
133
|
}),
|
|
126
134
|
wgPublicKey: cliparse.argument('public-key', {
|
|
127
135
|
metavar: 'public_key',
|
|
128
|
-
description: '
|
|
136
|
+
description: 'WireGuard public key of the external peer to link to a Network Group',
|
|
129
137
|
}),
|
|
130
138
|
email: cliparse.argument('email', {
|
|
131
139
|
description: 'Email address',
|
|
@@ -206,7 +214,7 @@ async function run() {
|
|
|
206
214
|
ngMembersIdsToLink: cliparse.option('link', {
|
|
207
215
|
metavar: 'members_ids',
|
|
208
216
|
description:
|
|
209
|
-
|
|
217
|
+
'Comma separated list of members IDs to link to a Network Group (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.)',
|
|
210
218
|
parser: Parsers.commaSeparated,
|
|
211
219
|
}),
|
|
212
220
|
ngResourceType: cliparse.option('type', {
|
|
@@ -1413,16 +1421,18 @@ async function run() {
|
|
|
1413
1421
|
const ngLinkCommand = cliparse.command(
|
|
1414
1422
|
'link',
|
|
1415
1423
|
{
|
|
1416
|
-
description:
|
|
1417
|
-
|
|
1424
|
+
description:
|
|
1425
|
+
'Link a resource by its ID (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.) to a Network Group',
|
|
1426
|
+
args: [args.ngResourceId, args.ngIdOrLabel],
|
|
1418
1427
|
},
|
|
1419
1428
|
ng.linkToNg,
|
|
1420
1429
|
);
|
|
1421
1430
|
const ngUnlinkCommand = cliparse.command(
|
|
1422
1431
|
'unlink',
|
|
1423
1432
|
{
|
|
1424
|
-
description:
|
|
1425
|
-
|
|
1433
|
+
description:
|
|
1434
|
+
'Unlink a resource by its ID (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.) from a Network Group',
|
|
1435
|
+
args: [args.ngResourceId, args.ngIdOrLabel],
|
|
1426
1436
|
},
|
|
1427
1437
|
ng.unlinkFromNg,
|
|
1428
1438
|
);
|
|
@@ -1438,7 +1448,7 @@ async function run() {
|
|
|
1438
1448
|
const ngGetConfigCommand = cliparse.command(
|
|
1439
1449
|
'get-config',
|
|
1440
1450
|
{
|
|
1441
|
-
description: 'Get the
|
|
1451
|
+
description: 'Get the WireGuard configuration of a peer in a Network Group',
|
|
1442
1452
|
args: [args.ngExternalIdOrLabel, args.ngIdOrLabel],
|
|
1443
1453
|
options: [opts.humanJsonOutputFormat],
|
|
1444
1454
|
},
|
|
@@ -1977,18 +1987,10 @@ async function run() {
|
|
|
1977
1987
|
},
|
|
1978
1988
|
database.listBackups,
|
|
1979
1989
|
);
|
|
1980
|
-
const databaseCommand = cliparse.command(
|
|
1981
|
-
'
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
commands: [backupsCommand],
|
|
1985
|
-
},
|
|
1986
|
-
async () => {
|
|
1987
|
-
console.info('This command is not available, you can try the following commands:');
|
|
1988
|
-
console.info('clever database backups');
|
|
1989
|
-
console.info('clever database backups download');
|
|
1990
|
-
},
|
|
1991
|
-
);
|
|
1990
|
+
const databaseCommand = cliparse.command('database', {
|
|
1991
|
+
description: 'Manage databases and backups',
|
|
1992
|
+
commands: [backupsCommand],
|
|
1993
|
+
});
|
|
1992
1994
|
|
|
1993
1995
|
// Patch help command description
|
|
1994
1996
|
cliparseCommands.helpCommand.description = 'Display help about the Clever Cloud CLI';
|
package/package.json
CHANGED
package/src/commands/addon.js
CHANGED
|
@@ -3,6 +3,7 @@ import { styleText } from '../lib/style-text.js';
|
|
|
3
3
|
import { getAllEnvVars } from '@clevercloud/client/esm/api/v2/addon.js';
|
|
4
4
|
import { toNameEqualsValueString } from '@clevercloud/client/esm/utils/env-vars.js';
|
|
5
5
|
import dedent from 'dedent';
|
|
6
|
+
import { getOperator } from '../clever-client/operators.js';
|
|
6
7
|
import { formatTable } from '../format-table.js';
|
|
7
8
|
import { Logger } from '../logger.js';
|
|
8
9
|
import * as Addon from '../models/addon.js';
|
|
@@ -53,6 +54,45 @@ export async function list(params) {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
const ADDON_PROVIDERS = {
|
|
58
|
+
keycloak: {
|
|
59
|
+
name: 'Keycloak',
|
|
60
|
+
isOperator: true,
|
|
61
|
+
postCreateInstructions: `Learn more about Keycloak on Clever Cloud: ${conf.DOC_URL}/addons/keycloak/`,
|
|
62
|
+
},
|
|
63
|
+
kv: {
|
|
64
|
+
name: 'Materia KV',
|
|
65
|
+
isOperator: false,
|
|
66
|
+
status: 'beta',
|
|
67
|
+
postCreateInstructions: (addonId) => dedent`
|
|
68
|
+
${styleText('yellow', "You can easily use Materia KV with 'redis-cli', with such commands:")}
|
|
69
|
+
${styleText('blue', `source <(clever addon env ${addonId} -F shell)`)}
|
|
70
|
+
${styleText('blue', 'redis-cli -h $KV_HOST -p $KV_PORT --tls')}
|
|
71
|
+
Learn more about Materia KV on Clever Cloud: ${conf.DOC_URL}/addons/materia-kv/
|
|
72
|
+
`,
|
|
73
|
+
},
|
|
74
|
+
'addon-matomo': {
|
|
75
|
+
name: 'Matomo',
|
|
76
|
+
isOperator: true,
|
|
77
|
+
postCreateInstructions: `Learn more about Matomo on Clever Cloud: ${conf.DOC_URL}/addons/matomo/`,
|
|
78
|
+
},
|
|
79
|
+
metabase: {
|
|
80
|
+
name: 'Metabase',
|
|
81
|
+
isOperator: true,
|
|
82
|
+
postCreateInstructions: `Learn more about Metabase on Clever Cloud: ${conf.DOC_URL}/addons/metabase/`,
|
|
83
|
+
},
|
|
84
|
+
otoroshi: {
|
|
85
|
+
name: 'Otoroshi with LLM',
|
|
86
|
+
isOperator: true,
|
|
87
|
+
postCreateInstructions: `Learn more about Otoroshi with LLM on Clever Cloud: ${conf.DOC_URL}/addons/otoroshi/`,
|
|
88
|
+
},
|
|
89
|
+
'addon-pulsar': {
|
|
90
|
+
name: 'Pulsar',
|
|
91
|
+
isOperator: false,
|
|
92
|
+
postCreateInstructions: `Learn more about Pulsar on Clever Cloud: ${conf.DOC_URL}/addons/pulsar/`,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
56
96
|
export async function create(params) {
|
|
57
97
|
const [providerName, name] = params.args;
|
|
58
98
|
const {
|
|
@@ -62,23 +102,21 @@ export async function create(params) {
|
|
|
62
102
|
yes: skipConfirmation,
|
|
63
103
|
org: orgaIdOrName,
|
|
64
104
|
format,
|
|
105
|
+
'addon-version': version,
|
|
106
|
+
option: addonOptions,
|
|
65
107
|
} = params.options;
|
|
66
|
-
const version = params.options['addon-version'];
|
|
67
|
-
const addonOptions = parseAddonOptions(params.options.option);
|
|
68
|
-
|
|
69
|
-
const ownerId = orgaIdOrName != null ? await Organisation.getId(orgaIdOrName) : await User.getCurrentId();
|
|
70
108
|
|
|
71
109
|
const addonToCreate = {
|
|
72
|
-
ownerId,
|
|
73
110
|
name,
|
|
74
111
|
providerName,
|
|
75
112
|
planName,
|
|
76
113
|
region,
|
|
77
114
|
skipConfirmation,
|
|
78
115
|
version,
|
|
79
|
-
addonOptions,
|
|
116
|
+
addonOptions: parseAddonOptions(addonOptions),
|
|
80
117
|
};
|
|
81
118
|
|
|
119
|
+
const ownerId = orgaIdOrName != null ? await Organisation.getId(orgaIdOrName) : await User.getCurrentId();
|
|
82
120
|
if (linkedAppAlias != null) {
|
|
83
121
|
const linkedAppData = await AppConfig.getAppDetails({ alias: linkedAppAlias });
|
|
84
122
|
if (orgaIdOrName != null && linkedAppData.ownerId !== ownerId && format === 'human') {
|
|
@@ -86,163 +124,98 @@ export async function create(params) {
|
|
|
86
124
|
'The specified application does not belong to the specified organisation. Ignoring the `--org` option',
|
|
87
125
|
);
|
|
88
126
|
}
|
|
89
|
-
|
|
90
|
-
...addonToCreate,
|
|
91
|
-
ownerId: linkedAppData.ownerId,
|
|
92
|
-
});
|
|
93
|
-
await Addon.link(linkedAppData.ownerId, linkedAppData.appId, { addon_id: newAddon.id });
|
|
94
|
-
displayAddon(
|
|
95
|
-
format,
|
|
96
|
-
newAddon,
|
|
97
|
-
providerName,
|
|
98
|
-
`Add-on created and linked to application ${linkedAppAlias} successfully!`,
|
|
99
|
-
);
|
|
127
|
+
addonToCreate.ownerId = linkedAppData.ownerId;
|
|
100
128
|
} else {
|
|
101
|
-
|
|
102
|
-
displayAddon(format, newAddon, providerName, 'Add-on created successfully!');
|
|
129
|
+
addonToCreate.ownerId = ownerId;
|
|
103
130
|
}
|
|
104
|
-
}
|
|
105
131
|
|
|
106
|
-
|
|
107
|
-
const PROVIDERS_WITH_URL = {
|
|
108
|
-
keycloak: {
|
|
109
|
-
name: 'Keycloak',
|
|
110
|
-
urlEnv: 'CC_KEYCLOAK_URL',
|
|
111
|
-
},
|
|
112
|
-
'addon-matomo': {
|
|
113
|
-
name: 'Matomo',
|
|
114
|
-
urlEnv: 'MATOMO_URL',
|
|
115
|
-
},
|
|
116
|
-
metabase: {
|
|
117
|
-
name: 'Metabase',
|
|
118
|
-
urlEnv: 'METABASE_URL',
|
|
119
|
-
},
|
|
120
|
-
otoroshi: {
|
|
121
|
-
name: 'Otoroshi with LLM',
|
|
122
|
-
urlEnv: 'CC_OTOROSHI_URL',
|
|
123
|
-
},
|
|
124
|
-
};
|
|
132
|
+
const newAddon = await Addon.create(addonToCreate);
|
|
125
133
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Learn more about Keycloak on Clever Cloud: ${conf.DOC_URL}/addons/keycloak/,
|
|
131
|
-
`,
|
|
132
|
-
},
|
|
133
|
-
kv: {
|
|
134
|
-
status: 'alpha',
|
|
135
|
-
postCreateInstructions: dedent`
|
|
136
|
-
${styleText('yellow', "You can easily use Materia KV with 'redis-cli', with such commands:")}
|
|
137
|
-
${styleText('blue', `source <(clever addon env ${addon.id} -F shell)`)}
|
|
138
|
-
${styleText('blue', 'redis-cli -h $KV_HOST -p $KV_PORT --tls')}
|
|
139
|
-
Learn more about Materia KV on Clever Cloud: ${conf.DOC_URL}/addons/materia-kv/
|
|
140
|
-
`,
|
|
141
|
-
},
|
|
142
|
-
'addon-matomo': {
|
|
143
|
-
status: '',
|
|
144
|
-
postCreateInstructions: dedent`
|
|
145
|
-
Learn more about Matomo on Clever Cloud: ${conf.DOC_URL}/addons/matomo/
|
|
146
|
-
`,
|
|
147
|
-
},
|
|
148
|
-
metabase: {
|
|
149
|
-
status: '',
|
|
150
|
-
postCreateInstructions: dedent`
|
|
151
|
-
Learn more about Metabase on Clever Cloud: ${conf.DOC_URL}/addons/metabase/
|
|
152
|
-
`,
|
|
153
|
-
},
|
|
154
|
-
otoroshi: {
|
|
155
|
-
status: '',
|
|
156
|
-
postCreateInstructions: dedent`
|
|
157
|
-
Learn more about Otoroshi with LLM on Clever Cloud: ${conf.DOC_URL}/addons/otoroshi/
|
|
158
|
-
`,
|
|
159
|
-
},
|
|
160
|
-
'addon-pulsar': {
|
|
161
|
-
status: 'beta',
|
|
162
|
-
postCreateInstructions: dedent`
|
|
163
|
-
Learn more about Pulsar on Clever Cloud: ${conf.DOC_URL}/addons/pulsar/
|
|
164
|
-
`,
|
|
165
|
-
},
|
|
166
|
-
};
|
|
134
|
+
if (linkedAppAlias != null) {
|
|
135
|
+
const linkedAppData = await AppConfig.getAppDetails({ alias: linkedAppAlias });
|
|
136
|
+
await Addon.link(linkedAppData.ownerId, linkedAppData.appId, { addon_id: newAddon.id });
|
|
137
|
+
}
|
|
167
138
|
|
|
168
|
-
|
|
139
|
+
const provider = ADDON_PROVIDERS[providerName];
|
|
169
140
|
let statusMessage = '';
|
|
170
|
-
if (
|
|
171
|
-
|
|
141
|
+
if (provider?.status) {
|
|
142
|
+
statusMessage = `The ${provider.name} provider is in ${provider.status} testing phase`;
|
|
143
|
+
if (provider.status === 'alpha') {
|
|
144
|
+
statusMessage += ". Don't store sensitive or production grade data.";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// JSON format
|
|
149
|
+
if (format === 'json') {
|
|
150
|
+
const jsonAddon = {
|
|
151
|
+
id: newAddon.id,
|
|
152
|
+
realId: newAddon.realId,
|
|
153
|
+
name: newAddon.name,
|
|
154
|
+
env: newAddon.env,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (provider?.status) {
|
|
158
|
+
jsonAddon.availability = provider.status;
|
|
159
|
+
jsonAddon.message = statusMessage;
|
|
160
|
+
}
|
|
172
161
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
WIP_PROVIDERS[providerName].status === 'alpha' ? ". Don't store sensitive or production grade data." : '';
|
|
162
|
+
Logger.printJson(jsonAddon);
|
|
163
|
+
return;
|
|
176
164
|
}
|
|
177
165
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
env: addon.env,
|
|
185
|
-
};
|
|
166
|
+
// Human format
|
|
167
|
+
if (linkedAppAlias != null) {
|
|
168
|
+
Logger.println(`Add-on created and linked to application ${linkedAppAlias} successfully!`);
|
|
169
|
+
} else {
|
|
170
|
+
Logger.println('Add-on created successfully!');
|
|
171
|
+
}
|
|
186
172
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
: jsonAddon,
|
|
191
|
-
);
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
173
|
+
Logger.println(`ID: ${newAddon.id}`);
|
|
174
|
+
Logger.println(`Real ID: ${newAddon.realId}`);
|
|
175
|
+
Logger.println(`Name: ${newAddon.name}`);
|
|
194
176
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (providerName in PROVIDERS_WITH_URL) {
|
|
200
|
-
const provider = PROVIDERS_WITH_URL[providerName];
|
|
201
|
-
const urlEnv = addon.env.find((entry) => entry.name === provider.urlEnv);
|
|
202
|
-
const urlToShow = urlEnv.value;
|
|
203
|
-
const urlToShowWithHttps = urlToShow.startsWith('http') ? urlToShow : `https://${urlToShow}`;
|
|
204
|
-
|
|
205
|
-
if (urlEnv) {
|
|
206
|
-
Logger.println();
|
|
207
|
-
Logger.println(`Your ${provider.name} is starting:`);
|
|
208
|
-
Logger.println(` - Access it: ${urlToShowWithHttps}`);
|
|
209
|
-
Logger.println(` - Manage it: ${conf.GOTO_URL}/${addon.id}`);
|
|
210
|
-
}
|
|
177
|
+
const operator = ADDON_PROVIDERS[providerName]?.isOperator
|
|
178
|
+
? await getOperator({ provider: providerName, realId: newAddon.realId }).then(sendToApi)
|
|
179
|
+
: null;
|
|
211
180
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
` - Temporary password: ${addon.env.find((e) => e.name === 'CC_KEYCLOAK_ADMIN_DEFAULT_PASSWORD').value}`,
|
|
220
|
-
);
|
|
221
|
-
}
|
|
181
|
+
if (operator) {
|
|
182
|
+
if (operator.accessUrl) {
|
|
183
|
+
Logger.println();
|
|
184
|
+
Logger.println(`Your ${ADDON_PROVIDERS[providerName].name} is starting:`);
|
|
185
|
+
Logger.println(` - Access it: ${operator.accessUrl}`);
|
|
186
|
+
Logger.println(` - Manage it: ${conf.GOTO_URL}/${newAddon.id}`);
|
|
187
|
+
}
|
|
222
188
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
` - Admin user name: ${addon.env.find((e) => e.name === 'CC_OTOROSHI_INITIAL_ADMIN_LOGIN').value}`,
|
|
230
|
-
);
|
|
231
|
-
Logger.println(
|
|
232
|
-
` - Initial password: ${addon.env.find((e) => e.name === 'CC_OTOROSHI_INITIAL_ADMIN_PASSWORD').value}`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
189
|
+
if (operator.initialCredentials) {
|
|
190
|
+
if (providerName === 'keycloak') {
|
|
191
|
+
Logger.println();
|
|
192
|
+
Logger.println("An initial account has been created, you'll be invited to change the password at first login:");
|
|
193
|
+
Logger.println(` - Admin user name: ${operator.initialCredentials.user}`);
|
|
194
|
+
Logger.println(` - Temporary password: ${operator.initialCredentials.password}`);
|
|
235
195
|
}
|
|
236
|
-
|
|
237
|
-
if (providerName in WIP_PROVIDERS) {
|
|
196
|
+
if (providerName === 'otoroshi') {
|
|
238
197
|
Logger.println();
|
|
198
|
+
Logger.println(
|
|
199
|
+
'An initial account has been created, change the password at first login (Security -> Administrators -> Edit user):',
|
|
200
|
+
);
|
|
201
|
+
Logger.println(` - Admin user name: ${operator.initialCredentials.user}`);
|
|
202
|
+
Logger.println(` - Password: ${operator.initialCredentials.password}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
239
206
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
207
|
+
if (provider?.postCreateInstructions) {
|
|
208
|
+
Logger.println();
|
|
243
209
|
|
|
244
|
-
|
|
245
|
-
}
|
|
210
|
+
if (statusMessage !== '') {
|
|
211
|
+
Logger.println(styleText('yellow', `/!\\ ${statusMessage}`));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (typeof provider.postCreateInstructions === 'function') {
|
|
215
|
+
Logger.println(provider.postCreateInstructions(newAddon.id));
|
|
216
|
+
} else {
|
|
217
|
+
Logger.println(provider.postCreateInstructions);
|
|
218
|
+
}
|
|
246
219
|
}
|
|
247
220
|
}
|
|
248
221
|
|
package/src/commands/diag.js
CHANGED
|
@@ -17,11 +17,9 @@ export async function diag(params) {
|
|
|
17
17
|
if (authDetails.token == null) {
|
|
18
18
|
return 'not connected';
|
|
19
19
|
}
|
|
20
|
-
|
|
21
20
|
if (userId == null) {
|
|
22
21
|
return 'authentication failed';
|
|
23
22
|
}
|
|
24
|
-
|
|
25
23
|
return 'authenticated';
|
|
26
24
|
}
|
|
27
25
|
|
|
@@ -32,7 +30,8 @@ export async function diag(params) {
|
|
|
32
30
|
platform: os.platform(),
|
|
33
31
|
release: os.release(),
|
|
34
32
|
arch: process.arch,
|
|
35
|
-
shell:
|
|
33
|
+
shell: getShell(),
|
|
34
|
+
terminal: getTerminal(),
|
|
36
35
|
isPackaged: process.pkg != null,
|
|
37
36
|
execPath: process.execPath,
|
|
38
37
|
configFile: conf.CONFIGURATION_FILE,
|
|
@@ -69,7 +68,8 @@ export async function diag(params) {
|
|
|
69
68
|
Logger.println('Linux ' + styleText('green', formattedDiag.linuxInfos));
|
|
70
69
|
}
|
|
71
70
|
Logger.println('Shell ' + styleText('green', formattedDiag.shell));
|
|
72
|
-
Logger.println('
|
|
71
|
+
Logger.println('Terminal ' + styleText('green', formattedDiag.terminal));
|
|
72
|
+
Logger.println('Packaged ' + styleText('green', formattedDiag.isPackaged));
|
|
73
73
|
Logger.println('Exec path ' + styleText('green', formattedDiag.execPath));
|
|
74
74
|
Logger.println('Config file ' + styleText('green', formattedDiag.configFile));
|
|
75
75
|
|
|
@@ -95,3 +95,32 @@ export async function diag(params) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
function getShell() {
|
|
100
|
+
const platform = os.platform();
|
|
101
|
+
|
|
102
|
+
if (platform === 'win32') {
|
|
103
|
+
if (process.env.PSModulePath) {
|
|
104
|
+
if (process.env.WT_SESSION) {
|
|
105
|
+
return 'PowerShell (Windows Terminal)';
|
|
106
|
+
}
|
|
107
|
+
if (process.env.PSVersionTable || process.env.POWERSHELL_DISTRIBUTION_CHANNEL) {
|
|
108
|
+
return 'PowerShell Core';
|
|
109
|
+
}
|
|
110
|
+
return 'Windows PowerShell';
|
|
111
|
+
}
|
|
112
|
+
if (process.env.WT_SESSION) {
|
|
113
|
+
return 'Windows Terminal (cmd)';
|
|
114
|
+
}
|
|
115
|
+
return process.env.ComSpec || 'cmd.exe';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return process.env.SHELL || 'unknown';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getTerminal() {
|
|
122
|
+
if (process.env.WT_SESSION) {
|
|
123
|
+
return 'Windows Terminal';
|
|
124
|
+
}
|
|
125
|
+
return process.env.TERM_PROGRAM || process.env.TERMINAL_EMULATOR || process.env.TERM;
|
|
126
|
+
}
|
package/src/commands/ng.js
CHANGED
|
@@ -47,7 +47,7 @@ export async function deleteNg(params) {
|
|
|
47
47
|
* @param {Object} params
|
|
48
48
|
* @param {Object} params.args[0] External peer ID or label
|
|
49
49
|
* @param {Object} params.args[1] Network Group ID or label
|
|
50
|
-
* @param {string} params.args[2]
|
|
50
|
+
* @param {string} params.args[2] WireGuard public key
|
|
51
51
|
* @param {Object} params.options.org Organisation ID or name
|
|
52
52
|
*/
|
|
53
53
|
export async function createExternalPeer(params) {
|
|
@@ -3,7 +3,7 @@ import { conf } from './models/configuration.js';
|
|
|
3
3
|
|
|
4
4
|
export const EXPERIMENTAL_FEATURES = {
|
|
5
5
|
kv: {
|
|
6
|
-
status: '
|
|
6
|
+
status: 'beta',
|
|
7
7
|
description:
|
|
8
8
|
'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
|
|
9
9
|
instructions: dedent`
|
|
@@ -20,20 +20,20 @@ export const EXPERIMENTAL_FEATURES = {
|
|
|
20
20
|
},
|
|
21
21
|
ng: {
|
|
22
22
|
status: 'beta',
|
|
23
|
-
description: 'Manage Network Groups to manage applications, add-ons, external peers through a
|
|
23
|
+
description: 'Manage Network Groups to manage applications, add-ons, external peers through a WireGuard network',
|
|
24
24
|
instructions: dedent`
|
|
25
25
|
- Create a Network Group:
|
|
26
26
|
clever ng create myNG
|
|
27
27
|
- Create a Network Group with members (application, database add-on):
|
|
28
|
-
clever ng create myNG --link app_xxx,
|
|
28
|
+
clever ng create myNG --link app_xxx,postgresql_xxx
|
|
29
29
|
- List Network Groups:
|
|
30
30
|
clever ng
|
|
31
31
|
- Delete a Network Group:
|
|
32
32
|
clever ng delete myNG
|
|
33
33
|
- (Un)Link an application or a database add-on to an existing Network Group:
|
|
34
34
|
clever ng link app_xxx myNG
|
|
35
|
-
clever ng unlink
|
|
36
|
-
- Get the
|
|
35
|
+
clever ng unlink postgresql_xxx myNG
|
|
36
|
+
- Get the WireGuard configuration of a peer:
|
|
37
37
|
clever ng get-config peerIdOrLabel myNG
|
|
38
38
|
- Get details about a Network Group, a member or a peer:
|
|
39
39
|
clever ng get myNg
|
package/src/models/log-v4.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
|
|
2
1
|
import * as networkGroupApi from '@clevercloud/client/esm/api/v4/network-group.js';
|
|
3
2
|
import crypto from 'node:crypto';
|
|
4
3
|
import { setTimeout } from 'node:timers/promises';
|
|
5
4
|
import { styleText } from '../lib/style-text.js';
|
|
6
5
|
import { Logger } from '../logger.js';
|
|
7
6
|
import * as networkGroup from './ng.js';
|
|
7
|
+
import { NG_MEMBER_PREFIXES } from './ng.js';
|
|
8
8
|
import { sendToApi } from './send-to-api.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -128,14 +128,16 @@ export async function deleteExternalPeerWithParent(ngIdOrLabel, peerIdOrLabel, o
|
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
130
|
* Link a Member to a Network Group
|
|
131
|
-
* @param {object} ngIdOrLabel The Network
|
|
131
|
+
* @param {object} ngIdOrLabel The Network Group ID or Label
|
|
132
132
|
* @param {string} memberId ID of the Member to link
|
|
133
133
|
* @param {object} org Organisation ID or name
|
|
134
134
|
* @param {string} label Label of the Member
|
|
135
135
|
*/
|
|
136
136
|
export async function linkMember(ngIdOrLabel, memberId, org, label) {
|
|
137
137
|
if (!memberId) {
|
|
138
|
-
throw new Error(
|
|
138
|
+
throw new Error(
|
|
139
|
+
'A valid member ID is required (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.)',
|
|
140
|
+
);
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
|
|
@@ -144,7 +146,7 @@ export async function linkMember(ngIdOrLabel, memberId, org, label) {
|
|
|
144
146
|
throw new Error(`Network Group ${styleText('red', ngIdOrLabel.ngId || ngIdOrLabel.ngResourceLabel)} not found`);
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
|
|
149
|
+
checkMembersToLink([memberId]);
|
|
148
150
|
|
|
149
151
|
const alreadyMember = ng.members.find((m) => m.id === memberId);
|
|
150
152
|
if (alreadyMember) {
|
|
@@ -186,7 +188,9 @@ export async function linkMember(ngIdOrLabel, memberId, org, label) {
|
|
|
186
188
|
*/
|
|
187
189
|
export async function unlinkMember(ngIdOrLabel, memberId, org) {
|
|
188
190
|
if (!memberId) {
|
|
189
|
-
throw new Error(
|
|
191
|
+
throw new Error(
|
|
192
|
+
'A valid member ID is required (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.)',
|
|
193
|
+
);
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
const [ng] = await networkGroup.searchNgOrResource(ngIdOrLabel, org, 'NetworkGroup');
|
|
@@ -215,39 +219,23 @@ export async function unlinkMember(ngIdOrLabel, memberId, org) {
|
|
|
215
219
|
|
|
216
220
|
/**
|
|
217
221
|
* Check if members can be linked to a Network Group
|
|
218
|
-
* @param {Array<string>}
|
|
222
|
+
* @param {Array<string>} memberIds Members to check
|
|
219
223
|
* @throws {Error} If members can't be linked to a Network Group
|
|
220
224
|
*/
|
|
221
|
-
export
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
const summary = await getSummary().then(sendToApi);
|
|
225
|
-
|
|
226
|
-
let data = summary.user;
|
|
227
|
-
if (summary.user.id !== ownerId) {
|
|
228
|
-
data = summary.organisations.find((o) => o.id === ownerId);
|
|
229
|
-
}
|
|
230
|
-
|
|
225
|
+
export function checkMembersToLink(memberIds) {
|
|
226
|
+
const validPrefixes = Object.keys(NG_MEMBER_PREFIXES);
|
|
231
227
|
const membersNotOK = [];
|
|
232
|
-
let source = data.applications;
|
|
233
|
-
|
|
234
|
-
for (const memberId of members) {
|
|
235
|
-
if (memberId.startsWith('addon_')) {
|
|
236
|
-
source = data.addons;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const foundRessource = source.find((r) => r.id === memberId);
|
|
240
228
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
229
|
+
for (const memberId of memberIds) {
|
|
230
|
+
const hasValidPrefix = validPrefixes.some((prefix) => memberId.startsWith(prefix));
|
|
231
|
+
if (!hasValidPrefix) {
|
|
244
232
|
membersNotOK.push(memberId);
|
|
245
233
|
}
|
|
246
234
|
}
|
|
247
235
|
|
|
248
236
|
if (membersNotOK.length > 0) {
|
|
249
237
|
throw new Error(
|
|
250
|
-
`Member(s) ${styleText('red', membersNotOK.join(', '))} can't be linked to the Network Group, check
|
|
238
|
+
`Member(s) ${styleText('red', membersNotOK.join(', '))} can't be linked to the Network Group, check member ID format`,
|
|
251
239
|
);
|
|
252
240
|
}
|
|
253
241
|
}
|
package/src/models/ng.js
CHANGED
|
@@ -17,9 +17,13 @@ import * as User from './user.js';
|
|
|
17
17
|
export const POLLING_TIMEOUT_MS = 30_000;
|
|
18
18
|
export const POLLING_INTERVAL_MS = 1000;
|
|
19
19
|
export const DOMAIN = 'cc-ng.cloud';
|
|
20
|
-
const
|
|
20
|
+
export const NG_MEMBER_PREFIXES = {
|
|
21
21
|
app_: 'APPLICATION',
|
|
22
|
-
|
|
22
|
+
elasticsearch_: 'ADDON',
|
|
23
|
+
mongodb_: 'ADDON',
|
|
24
|
+
mysql_: 'ADDON',
|
|
25
|
+
postgresql_: 'ADDON',
|
|
26
|
+
redis_: 'ADDON',
|
|
23
27
|
external_: 'EXTERNAL',
|
|
24
28
|
};
|
|
25
29
|
|
|
@@ -37,7 +41,7 @@ export async function create(label, description, tags, membersIds, orgaIdOrName)
|
|
|
37
41
|
const ownerId = await getOwnerIdFromOrgaIdOrName(orgaIdOrName);
|
|
38
42
|
|
|
39
43
|
if (membersIds?.length > 0) {
|
|
40
|
-
|
|
44
|
+
checkMembersToLink(membersIds);
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
const members = constructMembers(id, membersIds || []);
|
|
@@ -72,11 +76,11 @@ export async function destroy(ngIdOrLabel, orgaIdOrName) {
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
/**
|
|
75
|
-
* Get the
|
|
79
|
+
* Get the WireGuard configuration of a Network Group peer
|
|
76
80
|
* @param {object} peerIdOrLabel The Peer ID or Label
|
|
77
81
|
* @param {object} ngIdOrLabel The Network Group ID or Label
|
|
78
82
|
* @param {object} orgaIdOrName The owner ID or name
|
|
79
|
-
* @returns {Promise<Object>} The Peer
|
|
83
|
+
* @returns {Promise<Object>} The Peer WireGuard configuration
|
|
80
84
|
* @throws {Error} If the Peer is not found
|
|
81
85
|
* @throws {Error} If the Network Group is not found
|
|
82
86
|
* @throws {Error} If the Peer is not in the Network Group
|
|
@@ -116,7 +120,7 @@ export async function getPeerConfig(peerIdOrLabel, ngIdOrLabel, orgaIdOrName) {
|
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
/**
|
|
119
|
-
* Get a Network
|
|
123
|
+
* Get a Network Group from an owner with members and peers
|
|
120
124
|
* @param {string} networkGroupId The Network Group ID
|
|
121
125
|
* @param {string} orgaIdOrName The owner ID or name
|
|
122
126
|
* @returns {Promise<Array<Object>>} The Network Groups
|
|
@@ -187,7 +191,7 @@ export async function searchNgOrResource(idOrLabel, orgaIdOrName, type = 'all',
|
|
|
187
191
|
|
|
188
192
|
if (filtered.length > 1 && type !== 'all') {
|
|
189
193
|
throw new Error(`Multiple resources found for ${styleText('red', query)}, use ID instead:
|
|
190
|
-
${filtered.map((f) => ` • ${f.id} ${styleText('grey', `(${f.label} - ${f.type})`)}`).join('\n')}`);
|
|
194
|
+
${filtered.map((f) => ` • ${f.id} ${styleText('grey', `(${f.domainName || f.label} - ${f.type})`)}`).join('\n')}`);
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
// Deduplicate results
|
|
@@ -203,14 +207,9 @@ ${filtered.map((f) => ` • ${f.id} ${styleText('grey', `(${f.label} - ${f.type}
|
|
|
203
207
|
export function constructMembers(ngId, membersIds) {
|
|
204
208
|
return membersIds.map((id) => {
|
|
205
209
|
const domainName = `${id}.m.${ngId}.${DOMAIN}`;
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
id,
|
|
210
|
-
domainName,
|
|
211
|
-
// Get kind from prefix match in id (app_*, addon_*, external_*) or default to 'APPLICATION'
|
|
212
|
-
kind: prefixToType[Object.keys(prefixToType).find((p) => id.startsWith(p))] ?? TYPE_PREFIXES.app_,
|
|
213
|
-
};
|
|
210
|
+
const prefix = Object.keys(NG_MEMBER_PREFIXES).find((p) => id.startsWith(p));
|
|
211
|
+
const kind = NG_MEMBER_PREFIXES[prefix];
|
|
212
|
+
return { id, domainName, kind };
|
|
214
213
|
});
|
|
215
214
|
}
|
|
216
215
|
|
package/src/parsers.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import cliparse from 'cliparse';
|
|
2
2
|
import ISO8601 from 'iso8601-duration';
|
|
3
3
|
import * as Application from './models/application.js';
|
|
4
|
+
import { NG_MEMBER_PREFIXES } from './models/ng.js';
|
|
4
5
|
|
|
5
6
|
const addonOptionsRegex = /^[\w-]+=.+$/;
|
|
6
7
|
|
|
@@ -225,12 +226,12 @@ function parseSimpleDuration(durationStr) {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
|
|
228
|
-
// Network
|
|
229
|
+
// Network Groups parsers
|
|
229
230
|
export function ngResourceType(string) {
|
|
230
231
|
if (string.startsWith('ng_')) {
|
|
231
232
|
return cliparse.parsers.success({ ngId: string });
|
|
232
233
|
}
|
|
233
|
-
if (
|
|
234
|
+
if (Object.keys(NG_MEMBER_PREFIXES).some((prefix) => string.startsWith(prefix))) {
|
|
234
235
|
return cliparse.parsers.success({ memberId: string });
|
|
235
236
|
}
|
|
236
237
|
return cliparse.parsers.success({ ngResourceLabel: string });
|