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.
- package/bin/clever.js +106 -20
- package/package.json +1 -1
- package/src/clever-client/k8s.js +96 -0
- package/src/commands/addon.js +119 -146
- package/src/commands/diag.js +33 -4
- package/src/commands/k8s.js +193 -0
- package/src/commands/ng.js +1 -1
- package/src/experimental-features.js +32 -5
- package/src/lib/ascii.js +48 -0
- package/src/lib/k8s.js +126 -0
- package/src/lib/prompts.js +2 -0
- package/src/models/ids-resolver.js +11 -0
- package/src/models/log-v4.js +1 -1
- package/src/models/ng-resources.js +16 -28
- package/src/models/ng.js +19 -30
- package/src/parsers.js +5 -3
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
|
+
}
|
|
@@ -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
|
+
}
|
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) {
|
|
@@ -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: '
|
|
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
|
|
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,
|
|
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
|
|
36
|
-
- Get the
|
|
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
|
package/src/lib/ascii.js
ADDED
|
@@ -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
|
+
}
|
package/src/lib/prompts.js
CHANGED
|
@@ -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
|
+
}
|
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
|
}
|