clever-tools 4.1.0 → 4.3.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.
@@ -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
+ }
@@ -0,0 +1,193 @@
1
+ import { typewriterLogo } from '../lib/ascii.js';
2
+ import {
3
+ getK8sCluster,
4
+ isK8sClusterActive,
5
+ k8sAddPersistentStorage,
6
+ k8sCreate,
7
+ k8sDelete,
8
+ k8sGetConfig,
9
+ k8sList,
10
+ } from '../lib/k8s.js';
11
+ import { confirm } from '../lib/prompts.js';
12
+ import { styleText } from '../lib/style-text.js';
13
+ import { Logger } from '../logger.js';
14
+
15
+ const DEPLOY_POLL_DELAY_MS = 10000;
16
+
17
+ export async function create(params) {
18
+ const clusterName = params.args[0];
19
+ const orgIdOrName = params.options.org;
20
+
21
+ try {
22
+ const cluster = await k8sCreate(clusterName, orgIdOrName);
23
+
24
+ if (params.options.watch) {
25
+ await typewriterLogo();
26
+
27
+ let deployedCluster = cluster;
28
+ while (deployedCluster.status !== 'ACTIVE' && deployedCluster.status !== 'FAILED') {
29
+ Logger.println(
30
+ `⏳ Cluster status: ${styleText('yellow', deployedCluster.status)}. Waiting for ${DEPLOY_POLL_DELAY_MS / 1000}s before checking again...`,
31
+ );
32
+ await new Promise((resolve) => setTimeout(resolve, DEPLOY_POLL_DELAY_MS));
33
+
34
+ deployedCluster = await getK8sCluster(orgIdOrName, cluster.id);
35
+ }
36
+
37
+ Logger.println('');
38
+ switch (deployedCluster.status) {
39
+ case 'ACTIVE':
40
+ Logger.printSuccess(
41
+ `Cluster ${styleText('green', `${deployedCluster.name} (${deployedCluster.id})`)} deployed successfully`,
42
+ );
43
+ break;
44
+ case 'FAILED':
45
+ throw new Error(
46
+ `Cluster ${styleText('red', `${deployedCluster.name} (${deployedCluster.id})`)} deployment failed`,
47
+ );
48
+ default:
49
+ throw new Error(`Unexpected cluster status: ${deployedCluster.status}`);
50
+ }
51
+ } else {
52
+ Logger.println(`🚀 Cluster ${styleText('white', `${cluster.name} (${cluster.id})`)} is being deployed`);
53
+ }
54
+
55
+ const orgMessageComplement = orgIdOrName ? `--org "${orgIdOrName.orga_id || orgIdOrName.orga_name}"` : '';
56
+
57
+ Logger.println('');
58
+ Logger.println(
59
+ `You can get its information with ${styleText('blue', `clever k8s get ${cluster.id} ${orgMessageComplement}`)}`,
60
+ );
61
+ } catch (error) {
62
+ if (error.responseBody?.code === 'clever.core.quota-exceeded') {
63
+ throw new Error(
64
+ 'Failed to create Kubernetes cluster: your quota exceeded, contact support to increase your quota',
65
+ );
66
+ } else {
67
+ throw new Error(error.message);
68
+ }
69
+ }
70
+ }
71
+
72
+ export async function del(params) {
73
+ const [clusterIdOrName] = params.args;
74
+ const { org: orgIdOrName, yes: confirmDeletion } = params.options;
75
+
76
+ let proceedDeletion = false;
77
+ if (confirmDeletion) {
78
+ proceedDeletion = true;
79
+ } else {
80
+ proceedDeletion = await confirm(
81
+ `Are you sure you want to delete the Kubernetes cluster ${styleText(
82
+ 'blue',
83
+ clusterIdOrName.addon_name || clusterIdOrName.operator_id,
84
+ )}?`,
85
+ 'Kubernetes cluster deletion cancelled.',
86
+ );
87
+ }
88
+
89
+ if (proceedDeletion) {
90
+ await k8sDelete(orgIdOrName, clusterIdOrName);
91
+ Logger.printSuccess(
92
+ `Kubernetes cluster ${styleText('green', clusterIdOrName.addon_name || clusterIdOrName.operator_id)} successfully deleted`,
93
+ );
94
+ }
95
+ }
96
+
97
+ export async function list(params) {
98
+ const { format, org: orgIdOrName } = params.options;
99
+ const clusters = await k8sList(orgIdOrName, format);
100
+
101
+ switch (format) {
102
+ case 'json':
103
+ Logger.printJson(clusters);
104
+ break;
105
+ case 'human':
106
+ default:
107
+ if (clusters.length === 0) {
108
+ Logger.println(`🔎 No cluster found, create one with ${styleText('blue', `clever k8s create`)} command`);
109
+ return;
110
+ }
111
+
112
+ Logger.println(`🔎 Found ${clusters.length} cluster${clusters.length > 1 ? 's' : ''}:`);
113
+
114
+ Object.values(clusters).forEach((c) => {
115
+ Logger.println(` • ${styleText('white', `${c.name} (${c.id})`)} - ${c.status}`);
116
+ });
117
+ break;
118
+ }
119
+ }
120
+
121
+ export async function get(params) {
122
+ const [clusterIdOrName] = params.args;
123
+ const { format, org: orgIdOrName } = params.options;
124
+
125
+ const k8sInfo = await getK8sCluster(orgIdOrName, clusterIdOrName);
126
+
127
+ switch (format) {
128
+ case 'json':
129
+ Logger.printJson(k8sInfo);
130
+ break;
131
+ case 'human':
132
+ default:
133
+ console.table({
134
+ Name: k8sInfo.name,
135
+ ID: k8sInfo.id,
136
+ Version: k8sInfo.version,
137
+ Status: k8sInfo.status,
138
+ });
139
+
140
+ Logger.println('');
141
+ const orgMessageComplement = orgIdOrName ? `--org "${orgIdOrName.orga_id || orgIdOrName.orga_name}"` : '';
142
+
143
+ Logger.println(
144
+ `Once ACTIVE, get the kubeconfig with ${styleText('blue', `clever k8s get-kubeconfig ${k8sInfo.id} ${orgMessageComplement}`)}`,
145
+ );
146
+ break;
147
+ }
148
+ }
149
+
150
+ export async function addPersistentStorage(params) {
151
+ const [clusterIdOrName] = params.args;
152
+ const orgIdOrName = params.options.org;
153
+
154
+ if ((await isK8sClusterActive(orgIdOrName, clusterIdOrName)) === false) {
155
+ Logger.printInfo(
156
+ 'Persistent storage can only be added to deployed clusters, wait for the deployment to finish and try again',
157
+ );
158
+
159
+ const orgMessageComplement = orgIdOrName ? `--org "${orgIdOrName.orga_id || orgIdOrName.orga_name}"` : '';
160
+ Logger.println(
161
+ `Check with ${styleText('blue', `clever k8s get ${clusterIdOrName.addon_name || clusterIdOrName.operator_id} ${orgMessageComplement}`)}`,
162
+ );
163
+ return;
164
+ }
165
+
166
+ try {
167
+ await k8sAddPersistentStorage(orgIdOrName, clusterIdOrName);
168
+ Logger.printSuccess(
169
+ `Persistent storage successfully activated on cluster ${styleText('green', clusterIdOrName.addon_name || clusterIdOrName.operator_id)}`,
170
+ );
171
+ } catch (error) {
172
+ Logger.error("Failed to add persistent storage, check if it's not already activated");
173
+ }
174
+ }
175
+
176
+ export async function getConfig(params) {
177
+ const [clusterIdOrName] = params.args;
178
+ const orgIdOrName = params.options.org;
179
+
180
+ if ((await isK8sClusterActive(orgIdOrName, clusterIdOrName)) !== true) {
181
+ Logger.printInfo(
182
+ 'Kubeconfig can only be retrieved from deployed clusters, wait for the deployment to finish and try again',
183
+ );
184
+
185
+ const orgMessageComplement = orgIdOrName ? `--org "${orgIdOrName.orga_id || orgIdOrName.orga_name}"` : '';
186
+ Logger.println(
187
+ `Check with ${styleText('blue', `clever k8s get ${clusterIdOrName.addon_name || clusterIdOrName.operator_id} ${orgMessageComplement}`)}`,
188
+ );
189
+ return;
190
+ }
191
+
192
+ console.log(await k8sGetConfig(orgIdOrName, clusterIdOrName));
193
+ }
@@ -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) {
@@ -2,8 +2,35 @@ import dedent from 'dedent';
2
2
  import { conf } from './models/configuration.js';
3
3
 
4
4
  export const EXPERIMENTAL_FEATURES = {
5
+ k8s: {
6
+ status: 'beta',
7
+ description: 'Deploy and manage Kubernetes clusters on Clever Cloud',
8
+ instructions: dedent`
9
+ - Create a Kubernetes cluster:
10
+ clever k8s create my-cluster
11
+ clever k8s create my-cluster --org myOrg
12
+
13
+ - List Kubernetes clusters:
14
+ clever k8s list
15
+
16
+ - Get details about a Kubernetes cluster:
17
+ clever k8s get my-cluster
18
+
19
+ - Get kubeconfig file for a Kubernetes cluster:
20
+ clever k8s get-kubeconfig my-cluster
21
+ clever k8s get-kubeconfig my-cluster > ~/.kube/config
22
+
23
+ - Activate persistent storage on a Kubernetes cluster:
24
+ clever k8s add-persistent-storage my-cluster
25
+
26
+ - Delete a Kubernetes cluster:
27
+ clever k8s delete my-cluster
28
+
29
+ Learn more about Clever Kubernetes: ${conf.DOC_URL}/kubernetes/
30
+ `,
31
+ },
5
32
  kv: {
6
- status: 'alpha',
33
+ status: 'beta',
7
34
  description:
8
35
  'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
9
36
  instructions: dedent`
@@ -20,20 +47,20 @@ export const EXPERIMENTAL_FEATURES = {
20
47
  },
21
48
  ng: {
22
49
  status: 'beta',
23
- description: 'Manage Network Groups to manage applications, add-ons, external peers through a Wireguard network',
50
+ description: 'Manage Network Groups to manage applications, add-ons, external peers through a WireGuard network',
24
51
  instructions: dedent`
25
52
  - Create a Network Group:
26
53
  clever ng create myNG
27
54
  - Create a Network Group with members (application, database add-on):
28
- clever ng create myNG --link app_xxx,addon_xxx
55
+ clever ng create myNG --link app_xxx,postgresql_xxx
29
56
  - List Network Groups:
30
57
  clever ng
31
58
  - Delete a Network Group:
32
59
  clever ng delete myNG
33
60
  - (Un)Link an application or a database add-on to an existing Network Group:
34
61
  clever ng link app_xxx myNG
35
- clever ng unlink addon_xxx myNG
36
- - Get the Wireguard configuration of a peer:
62
+ clever ng unlink postgresql_xxx myNG
63
+ - Get the WireGuard configuration of a peer:
37
64
  clever ng get-config peerIdOrLabel myNG
38
65
  - Get details about a Network Group, a member or a peer:
39
66
  clever ng get myNg
@@ -0,0 +1,48 @@
1
+ import { styleText } from './style-text.js';
2
+
3
+ const LOGO = styleText(
4
+ 'green',
5
+ `
6
+ ██████╗██╗ ███████╗██╗ ██╗███████╗██████╗ ██╗ ██╗ █████╗ ███████╗
7
+ ██╔════╝██║ ██╔════╝██║ ██║██╔════╝██╔══██╗ ██║ ██╔╝██╔══██╗██╔════╝
8
+ ██║ ██║ █████╗ ██║ ██║█████╗ ██████╔╝ █████╔╝ ╚█████╔╝███████╗
9
+ ██║ ██║ ██╔══╝ ╚██╗ ██╔╝██╔══╝ ██╔══██╗ ██╔═██╗ ██╔══██╗╚════██║
10
+ ╚██████╗███████╗███████╗ ╚████╔╝ ███████╗██║ ██║ ██║ ██╗╚█████╔╝███████║
11
+ ╚═════╝╚══════╝╚══════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚════╝ ╚══════╝
12
+
13
+ Your Clever Cloud Kubernetes cluster has been successfully created, it's now starting up!
14
+
15
+ It will take about 1 minute. To manage it, use the following commands:
16
+ - clever k8s list List your Kubernetes clusters
17
+ - clever k8s get <cluster-id-or-name> Get information about a specific cluster
18
+ - clever k8s get-kubeconfig <cluster-id-or-name> Get the kubeconfig file for your cluster
19
+ - clever k8s delete <cluster-id-or-name> Delete a specific cluster
20
+
21
+ Learn more about commands with 'clever k8s --help'
22
+
23
+ For more information, read the documentation https://www.clever.cloud/developers/doc/kubernetes/
24
+
25
+ Enjoy!
26
+ `,
27
+ );
28
+
29
+ // const clearScreen = () => process.stdout.write('\x1B[2J\x1B[0f');
30
+ const hideCursor = () => process.stdout.write('\x1B[?25l');
31
+ const showCursor = () => process.stdout.write('\x1B[?25h');
32
+ const sleep = (ms = 42) => new Promise((resolve) => setTimeout(resolve, ms));
33
+
34
+ export async function typewriterLogo() {
35
+ hideCursor();
36
+
37
+ const lines = LOGO.split('\n');
38
+
39
+ for (const line of lines) {
40
+ for (const char of line) {
41
+ process.stdout.write(char);
42
+ await sleep();
43
+ }
44
+ console.log();
45
+ }
46
+
47
+ showCursor();
48
+ }
package/src/lib/k8s.js ADDED
@@ -0,0 +1,126 @@
1
+ import { styleText } from './style-text.js';
2
+
3
+ import {
4
+ addK8sPersistentStorage,
5
+ createK8sCluster,
6
+ deleteK8sCluster,
7
+ getK8sAddon,
8
+ getK8sConfig,
9
+ listK8sClusters,
10
+ } from '../clever-client/k8s.js';
11
+ import { getOwnerIdFromOrgIdOrName } from '../models/ids-resolver.js';
12
+ import { sendToApi } from '../models/send-to-api.js';
13
+
14
+ /**
15
+ * Check if a Kubernetes cluster status is ACTIVE
16
+ * @param {string} orgIdOrName The organisation ID or name
17
+ * @param {string} clusterIdOrName The cluster ID or name
18
+ * @returns {Promise<boolean>} True if the cluster is deployed, false otherwise
19
+ */
20
+ export async function isK8sClusterActive(orgIdOrName, clusterIdOrName) {
21
+ const ownerId = await getOwnerIdFromOrgIdOrName(orgIdOrName);
22
+ const clusterId = await getClusterIdFromAddonIdOrName(clusterIdOrName, ownerId);
23
+ const cluster = await getK8sAddon({ ownerId, clusterId }).then(sendToApi);
24
+
25
+ return cluster.status === 'ACTIVE';
26
+ }
27
+
28
+ /**
29
+ * Create a kubernetes cluster
30
+ * @param {string} name The name of the cluster
31
+ * @param {string} ownerId The owner ID
32
+ * @returns {Promise<void>}
33
+ */
34
+ export async function k8sCreate(name, ownerId) {
35
+ ownerId = await getOwnerIdFromOrgIdOrName(ownerId);
36
+
37
+ return createK8sCluster({ ownerId }, { name }).then(sendToApi);
38
+ }
39
+
40
+ /**
41
+ * List all kubernetes addons
42
+ * @param {string} format The output format
43
+ * @returns {Promise<void>}
44
+ */
45
+ export async function k8sList(orgIdOrName, format) {
46
+ const ownerId = await getOwnerIdFromOrgIdOrName(orgIdOrName);
47
+ const deployed = await listK8sClusters({ ownerId }).then(sendToApi);
48
+
49
+ return deployed.filter((op) => op.status != 'DELETED');
50
+ }
51
+
52
+ /**
53
+ * Get information about a kubernetes cluster
54
+ * @param {string} orgIdOrName The organisation ID or name
55
+ * @param {string} clusterIdOrName The cluster ID or name
56
+ * @returns {Promise<object>} The kubernetes cluster information
57
+ */
58
+ export async function getK8sCluster(orgIdOrName, clusterIdOrName) {
59
+ const ownerId = await getOwnerIdFromOrgIdOrName(orgIdOrName);
60
+ const clusterId = await getClusterIdFromAddonIdOrName(clusterIdOrName, ownerId);
61
+
62
+ return getK8sAddon({ ownerId, clusterId }).then(sendToApi);
63
+ }
64
+
65
+ /**
66
+ * Get Kubernetes cluster configuration
67
+ * @param {string} orgIdOrName The organisation ID or name
68
+ * @param {string} clusterIdOrName The cluster ID or name
69
+ * @returns {Promise<string>} The kubeconfig.yaml content
70
+ */
71
+ export async function k8sGetConfig(orgIdOrName, clusterIdOrName) {
72
+ const ownerId = await getOwnerIdFromOrgIdOrName(orgIdOrName);
73
+ const clusterId = await getClusterIdFromAddonIdOrName(clusterIdOrName, ownerId);
74
+
75
+ return getK8sConfig({ ownerId, clusterId }).then(sendToApi);
76
+ }
77
+
78
+ /**
79
+ * Delete a kubernetes cluster
80
+ * @param {string} orgIdOrName The organisation ID or name
81
+ * @param {string} clusterIdOrName The cluster ID or name
82
+ * @returns {Promise<void>}
83
+ */
84
+ export async function k8sDelete(orgIdOrName, clusterIdOrName) {
85
+ const ownerId = await getOwnerIdFromOrgIdOrName(orgIdOrName);
86
+ const clusterId = await getClusterIdFromAddonIdOrName(clusterIdOrName, ownerId);
87
+
88
+ return deleteK8sCluster({ ownerId, clusterId }).then(sendToApi);
89
+ }
90
+
91
+ /**
92
+ * Get Kubernetes cluster ID from an addon ID or name
93
+ * @param {string|object} addonIdOrName The addon ID or name
94
+ * @param {string} ownerId The owner ID
95
+ * @returns {Promise<string>} The Kubernetes cluster ID
96
+ */
97
+ export async function getClusterIdFromAddonIdOrName(addonIdOrName, ownerId) {
98
+ if (typeof addonIdOrName === 'string') {
99
+ return addonIdOrName;
100
+ } else if (typeof addonIdOrName === 'object' && addonIdOrName.operator_id) {
101
+ return addonIdOrName.operator_id;
102
+ } else if (typeof addonIdOrName === 'object' && addonIdOrName.addon_name) {
103
+ const clusters = await listK8sClusters({ ownerId }).then(sendToApi);
104
+ const matchingCluster = clusters.find((cluster) => cluster.name === addonIdOrName.addon_name);
105
+ if (matchingCluster) {
106
+ return matchingCluster.id;
107
+ } else {
108
+ throw new Error(`No Kubernetes cluster found with the name ${styleText('red', addonIdOrName.addon_name)}`);
109
+ }
110
+ } else {
111
+ throw new Error('Invalid Kubernetes Cluster identifier provided');
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Add persistent storage to a deployed Kubernetes cluster
117
+ * @param {string} orgIdOrName The organisation ID or name
118
+ * @param {string} clusterIdOrName The cluster ID or name
119
+ * @returns {Promise<void>}
120
+ */
121
+ export async function k8sAddPersistentStorage(orgIdOrName, clusterIdOrName) {
122
+ const ownerId = await getOwnerIdFromOrgIdOrName(orgIdOrName);
123
+ const clusterId = await getClusterIdFromAddonIdOrName(clusterIdOrName, ownerId);
124
+
125
+ return addK8sPersistentStorage({ ownerId, clusterId }).then(sendToApi);
126
+ }
@@ -9,6 +9,8 @@ export async function confirm(message, rejectionMessage) {
9
9
  if (!answer) {
10
10
  throw new Error(rejectionMessage);
11
11
  }
12
+
13
+ return answer;
12
14
  }
13
15
 
14
16
  export async function confirmAnswer(message, rejectionMessage, expectedAnswer) {
@@ -1,6 +1,8 @@
1
1
  import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
2
2
  import { Logger } from '../logger.js';
3
+ import * as User from '../models/user.js';
3
4
  import { loadIdsCache, writeIdsCache } from './configuration.js';
5
+ import * as Organisation from './organisation.js';
4
6
  import { sendToApi } from './send-to-api.js';
5
7
 
6
8
  /*
@@ -171,3 +173,12 @@ export async function findAddonsByAddonProvider(provider) {
171
173
 
172
174
  return candidates;
173
175
  }
176
+
177
+ /**
178
+ * Get the owner ID from an Organisation ID or name
179
+ * @param {object} orgIdOrName The Organisation ID or name
180
+ * @returns {Promise<string>} The owner ID
181
+ */
182
+ export async function getOwnerIdFromOrgIdOrName(orgIdOrName) {
183
+ return orgIdOrName != null ? Organisation.getId(orgIdOrName) : User.getCurrentId();
184
+ }
@@ -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
  }