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 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 group, a member or an (external) peer',
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: 'Wireguard public key of the external peer to link to a Network Group',
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
- "Comma separated list of members IDs to link to a Network Group ('app_xxx', 'addon_xxx', 'external_xxx')",
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: 'Link an application or a database add-on by its ID to a Network Group',
1417
- args: [args.ngAnyIdOrLabel, args.ngIdOrLabel],
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: 'Unlink an application or a database add-on by its ID from a Network Group',
1425
- args: [args.ngAnyIdOrLabel, args.ngIdOrLabel],
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 Wireguard configuration of a peer in a Network Group',
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
- 'database',
1982
- {
1983
- description: 'List available databases',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clever-tools",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Command Line Interface for Clever Cloud.",
5
5
  "keywords": [
6
6
  "cli",
@@ -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
- const newAddon = await Addon.create({
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
- const newAddon = await Addon.create(addonToCreate);
102
- displayAddon(format, newAddon, providerName, 'Add-on created successfully!');
129
+ addonToCreate.ownerId = ownerId;
103
130
  }
104
- }
105
131
 
106
- function displayAddon(format, addon, providerName, message) {
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
- const WIP_PROVIDERS = {
127
- keycloak: {
128
- status: '',
129
- postCreateInstructions: dedent`
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
- let providerNameToShow = '';
139
+ const provider = ADDON_PROVIDERS[providerName];
169
140
  let statusMessage = '';
170
- if (providerName in WIP_PROVIDERS && WIP_PROVIDERS[providerName].status !== '') {
171
- providerNameToShow = providerName === 'kv' ? 'Materia KV' : providerName;
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
- statusMessage = `The ${providerNameToShow} provider is in ${WIP_PROVIDERS[providerName].status} testing phase`;
174
- statusMessage +=
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
- switch (format) {
179
- case 'json': {
180
- const jsonAddon = {
181
- id: addon.id,
182
- realId: addon.realId,
183
- name: addon.name,
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
- Logger.printJson(
188
- WIP_PROVIDERS[providerName] != null
189
- ? { ...jsonAddon, availability: WIP_PROVIDERS[providerName].status, message: statusMessage }
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
- case 'human':
196
- default:
197
- Logger.println([message, `ID: ${addon.id}`, `Real ID: ${addon.realId}`, `Name: ${addon.name}`].join('\n'));
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
- if (providerName === 'keycloak') {
213
- Logger.println();
214
- Logger.println(
215
- "An initial account has been created, you'll be invited to change the password at first login:",
216
- );
217
- Logger.println(` - Admin user name: ${addon.env.find((e) => e.name === 'CC_KEYCLOAK_ADMIN').value}`);
218
- Logger.println(
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
- if (providerName === 'otoroshi') {
224
- Logger.println();
225
- Logger.println(
226
- 'An initial account has been created, change the password at first login (Security -> Administrators -> Edit user):',
227
- );
228
- Logger.println(
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
- if (statusMessage !== '') {
241
- Logger.println(styleText('yellow', `/!\\ ${statusMessage}`));
242
- }
207
+ if (provider?.postCreateInstructions) {
208
+ Logger.println();
243
209
 
244
- Logger.println(WIP_PROVIDERS[providerName].postCreateInstructions);
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
 
@@ -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: process.env.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('Packaged ' + styleText('green', String(formattedDiag.isPackaged)));
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
+ }
@@ -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] Wireguard public key
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: 'alpha',
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 Wireguard network',
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,addon_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 addon_xxx myNG
36
- - Get the Wireguard configuration of a peer:
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
@@ -65,7 +65,7 @@ export async function displayLogs(params) {
65
65
  jsonArray.push(log);
66
66
  return;
67
67
  case 'json-stream':
68
- Logger.printJson(log);
68
+ Logger.println(JSON.stringify(log));
69
69
  return;
70
70
  case 'human':
71
71
  default:
@@ -1,10 +1,10 @@
1
- import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
2
1
  import * as networkGroupApi from '@clevercloud/client/esm/api/v4/network-group.js';
3
2
  import crypto from 'node:crypto';
4
3
  import { setTimeout } from 'node:timers/promises';
5
4
  import { styleText } from '../lib/style-text.js';
6
5
  import { Logger } from '../logger.js';
7
6
  import * as networkGroup from './ng.js';
7
+ import { NG_MEMBER_PREFIXES } from './ng.js';
8
8
  import { sendToApi } from './send-to-api.js';
9
9
 
10
10
  /**
@@ -128,14 +128,16 @@ export async function deleteExternalPeerWithParent(ngIdOrLabel, peerIdOrLabel, o
128
128
 
129
129
  /**
130
130
  * Link a Member to a Network Group
131
- * @param {object} ngIdOrLabel The Network group ID or Label
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('A valid member ID is required (addon_xxx, app_xxx, external_xxx)');
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
- await checkMembersToLink([memberId], ng.ownerId);
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('A valid member ID is required (addon_xxx, app_xxx, external_xxx)');
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>} members Members to check
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 async function checkMembersToLink(members, ownerId) {
222
- const VALID_ADDON_PROVIDERS = ['es-addon', 'mongodb-addon', 'mysql-addon', 'postgresql-addon', 'redis-addon'];
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
- if (foundRessource && memberId.startsWith('addon_') && !VALID_ADDON_PROVIDERS.includes(foundRessource.providerId)) {
242
- membersNotOK.push(memberId);
243
- } else if (!foundRessource && !memberId.startsWith('external_')) {
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 Organisation ID or name`,
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 TYPE_PREFIXES = {
20
+ export const NG_MEMBER_PREFIXES = {
21
21
  app_: 'APPLICATION',
22
- addon_: 'ADDON',
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
- await checkMembersToLink(membersIds, ownerId);
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 Wireguard configuration of a Network Group peer
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 Wireguard configuration
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 group from an owner with members and peers
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 prefixToType = TYPE_PREFIXES;
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 groups parsers
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 (string.startsWith('app_') || string.startsWith('addon_') || string.startsWith('external_')) {
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 });