clever-tools 4.2.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 +84 -0
- package/package.json +1 -1
- package/src/clever-client/k8s.js +96 -0
- package/src/commands/k8s.js +193 -0
- package/src/experimental-features.js +27 -0
- 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/ng.js +5 -15
- package/src/parsers.js +2 -1
package/bin/clever.js
CHANGED
|
@@ -28,6 +28,7 @@ import * as drain from '../src/commands/drain.js';
|
|
|
28
28
|
import * as emails from '../src/commands/emails.js';
|
|
29
29
|
import * as env from '../src/commands/env.js';
|
|
30
30
|
import * as features from '../src/commands/features.js';
|
|
31
|
+
import * as k8s from '../src/commands/k8s.js';
|
|
31
32
|
import * as keycloak from '../src/commands/keycloak.js';
|
|
32
33
|
import * as kv from '../src/commands/kv.js';
|
|
33
34
|
import * as link from '../src/commands/link.js';
|
|
@@ -99,6 +100,11 @@ function colorizeExperimentalCommand(command, id) {
|
|
|
99
100
|
async function run() {
|
|
100
101
|
// ARGUMENTS
|
|
101
102
|
const args = {
|
|
103
|
+
k8sClusterName: cliparse.argument('cluster-name', { description: 'Kubernetes cluster name' }),
|
|
104
|
+
k8sIdOrName: cliparse.argument('id-or-name', {
|
|
105
|
+
description: 'Kubernetes cluster ID or name',
|
|
106
|
+
parser: Parsers.addonIdOrName,
|
|
107
|
+
}),
|
|
102
108
|
kvRawCommand: cliparse.argument('command', {
|
|
103
109
|
description: 'The raw command to send to the Materia KV or Redis® add-on',
|
|
104
110
|
}),
|
|
@@ -196,6 +202,13 @@ async function run() {
|
|
|
196
202
|
|
|
197
203
|
// OPTIONS
|
|
198
204
|
const opts = {
|
|
205
|
+
k8sDeployWatch: cliparse.flag('watch', {
|
|
206
|
+
aliases: ['w'],
|
|
207
|
+
description: 'Watch the deployment until the cluster is deployed',
|
|
208
|
+
}),
|
|
209
|
+
kubeConfigForceDownload: cliparse.flag('force', {
|
|
210
|
+
description: 'Force the download of the kubeconfig file, even if it already exists',
|
|
211
|
+
}),
|
|
199
212
|
targetVersion: cliparse.option('target', {
|
|
200
213
|
metavar: 'version',
|
|
201
214
|
description: 'Target version to upgrade to (e.g.: 24, 2.4, 2.4.1)',
|
|
@@ -1043,6 +1056,73 @@ async function run() {
|
|
|
1043
1056
|
features.list,
|
|
1044
1057
|
);
|
|
1045
1058
|
|
|
1059
|
+
// K8S COMMAND
|
|
1060
|
+
const k8sListCommand = cliparse.command(
|
|
1061
|
+
'list',
|
|
1062
|
+
{
|
|
1063
|
+
description: 'List Kubernetes clusters',
|
|
1064
|
+
options: [opts.orgaIdOrName, opts.humanJsonOutputFormat],
|
|
1065
|
+
},
|
|
1066
|
+
k8s.list,
|
|
1067
|
+
);
|
|
1068
|
+
const k8sCreateCommand = cliparse.command(
|
|
1069
|
+
'create',
|
|
1070
|
+
{
|
|
1071
|
+
description: 'Create a Kubernetes cluster',
|
|
1072
|
+
args: [args.k8sClusterName],
|
|
1073
|
+
options: [opts.orgaIdOrName, opts.k8sDeployWatch],
|
|
1074
|
+
},
|
|
1075
|
+
k8s.create,
|
|
1076
|
+
);
|
|
1077
|
+
const k8sDeleteCommand = cliparse.command(
|
|
1078
|
+
'delete',
|
|
1079
|
+
{
|
|
1080
|
+
description: 'Delete a Kubernetes cluster',
|
|
1081
|
+
options: [opts.orgaIdOrName, opts.confirmAddonDeletion],
|
|
1082
|
+
args: [args.k8sIdOrName],
|
|
1083
|
+
},
|
|
1084
|
+
k8s.del,
|
|
1085
|
+
);
|
|
1086
|
+
const k8sGetCommand = cliparse.command(
|
|
1087
|
+
'get',
|
|
1088
|
+
{
|
|
1089
|
+
description: 'Get information about a Kubernetes cluster',
|
|
1090
|
+
args: [args.k8sIdOrName],
|
|
1091
|
+
options: [opts.orgaIdOrName, opts.humanJsonOutputFormat],
|
|
1092
|
+
},
|
|
1093
|
+
k8s.get,
|
|
1094
|
+
);
|
|
1095
|
+
const k8sGetConfigCommand = cliparse.command(
|
|
1096
|
+
'get-kubeconfig',
|
|
1097
|
+
{
|
|
1098
|
+
description: 'Get configuration of a Kubernetes cluster',
|
|
1099
|
+
args: [args.k8sIdOrName],
|
|
1100
|
+
options: [opts.orgaIdOrName],
|
|
1101
|
+
},
|
|
1102
|
+
k8s.getConfig,
|
|
1103
|
+
);
|
|
1104
|
+
const k8sAddPersistentStorageCommand = cliparse.command(
|
|
1105
|
+
'add-persistent-storage',
|
|
1106
|
+
{
|
|
1107
|
+
description: 'Activate persistent storage to a deployed Kubernetes cluster',
|
|
1108
|
+
args: [args.k8sIdOrName],
|
|
1109
|
+
options: [opts.orgaIdOrName],
|
|
1110
|
+
},
|
|
1111
|
+
k8s.addPersistentStorage,
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
const k8sCommand = cliparse.command('k8s', {
|
|
1115
|
+
description: 'Manage Kubernetes clusters',
|
|
1116
|
+
commands: [
|
|
1117
|
+
k8sListCommand,
|
|
1118
|
+
k8sCreateCommand,
|
|
1119
|
+
k8sAddPersistentStorageCommand,
|
|
1120
|
+
k8sDeleteCommand,
|
|
1121
|
+
k8sGetCommand,
|
|
1122
|
+
k8sGetConfigCommand,
|
|
1123
|
+
],
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1046
1126
|
// KEYCLOAK COMMAND
|
|
1047
1127
|
const keycloakGetCommand = cliparse.command(
|
|
1048
1128
|
'get',
|
|
@@ -2048,6 +2128,10 @@ async function run() {
|
|
|
2048
2128
|
commands.push(colorizeExperimentalCommand(otoroshiCommand, 'operators'));
|
|
2049
2129
|
}
|
|
2050
2130
|
|
|
2131
|
+
if (featuresFromConf.k8s) {
|
|
2132
|
+
commands.push(colorizeExperimentalCommand(k8sCommand, 'k8s'));
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2051
2135
|
if (featuresFromConf.kv) {
|
|
2052
2136
|
commands.push(colorizeExperimentalCommand(kvRawCommand, 'kv'));
|
|
2053
2137
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// TODO: Move this to the Clever Cloud JS Client
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* POST /v4/kubernetes/organisations/{ownerId}/clusters
|
|
5
|
+
* @param {Object} params
|
|
6
|
+
* @param {String} params.ownerId
|
|
7
|
+
* @param {Object} body
|
|
8
|
+
*/
|
|
9
|
+
export function createK8sCluster(params, body) {
|
|
10
|
+
return Promise.resolve({
|
|
11
|
+
method: 'post',
|
|
12
|
+
url: `/v4/kubernetes/organisations/${params.ownerId}/clusters`,
|
|
13
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
14
|
+
// no queryParams
|
|
15
|
+
body,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* DELETE /v4/kubernetes/organisations/{ownerId}/clusters/{clusterId}
|
|
21
|
+
* @param {Object} params
|
|
22
|
+
* @param {String} params.ownerId
|
|
23
|
+
* @param {String} params.clusterId
|
|
24
|
+
*/
|
|
25
|
+
export function deleteK8sCluster(params) {
|
|
26
|
+
return Promise.resolve({
|
|
27
|
+
method: 'delete',
|
|
28
|
+
url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.clusterId}`,
|
|
29
|
+
headers: { Accept: 'application/json' },
|
|
30
|
+
// no queryParams
|
|
31
|
+
// no body
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* GET /v4/kubernetes/organisations/{ownerId}/clusters
|
|
37
|
+
* @param {Object} params
|
|
38
|
+
* @param {String} params.ownerId
|
|
39
|
+
*/
|
|
40
|
+
export function listK8sClusters(params) {
|
|
41
|
+
return Promise.resolve({
|
|
42
|
+
method: 'get',
|
|
43
|
+
url: `/v4/kubernetes/organisations/${params.ownerId}/clusters`,
|
|
44
|
+
headers: { Accept: 'application/json' },
|
|
45
|
+
// no queryParams
|
|
46
|
+
// no body
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* GET /v4/kubernetes/organisations/{ownerId}/clusters/{clusterId}
|
|
52
|
+
* @param {Object} params
|
|
53
|
+
* @param {String} params.ownerId
|
|
54
|
+
* @param {String} params.clusterId
|
|
55
|
+
*/
|
|
56
|
+
export function getK8sAddon(params) {
|
|
57
|
+
return Promise.resolve({
|
|
58
|
+
method: 'get',
|
|
59
|
+
url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.clusterId}`,
|
|
60
|
+
headers: { Accept: 'application/json' },
|
|
61
|
+
// no queryParams
|
|
62
|
+
// no body
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* GET /v4/kubernetes/organisations/{ownerId}/clusters/{clusterId}/kubeconfig.yaml
|
|
68
|
+
* @param {Object} params
|
|
69
|
+
* @param {String} params.ownerId
|
|
70
|
+
* @param {String} params.clusterId
|
|
71
|
+
*/
|
|
72
|
+
export function getK8sConfig(params) {
|
|
73
|
+
return Promise.resolve({
|
|
74
|
+
method: 'get',
|
|
75
|
+
url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.clusterId}/kubeconfig.yaml`,
|
|
76
|
+
// headers: { Accept: 'application/x-yaml' },
|
|
77
|
+
// no queryParams
|
|
78
|
+
// no body
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* POST /v4/kubernetes/organisations/ownerId}/clusters/{clusterId}/csi/ceph
|
|
84
|
+
* @param {Object} params
|
|
85
|
+
* @param {String} params.ownerId
|
|
86
|
+
* @param {String} params.clusterId
|
|
87
|
+
*/
|
|
88
|
+
export function addK8sPersistentStorage(params) {
|
|
89
|
+
return Promise.resolve({
|
|
90
|
+
method: 'post',
|
|
91
|
+
url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.clusterId}/csi/ceph`,
|
|
92
|
+
headers: { Accept: 'application/json' },
|
|
93
|
+
// no queryParams
|
|
94
|
+
// no body
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -2,6 +2,33 @@ 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
33
|
status: 'beta',
|
|
7
34
|
description:
|
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/ng.js
CHANGED
|
@@ -9,10 +9,9 @@ import crypto from 'node:crypto';
|
|
|
9
9
|
import { searchNetworkGroupOrResource } from '../clever-client/ng.js';
|
|
10
10
|
import { styleText } from '../lib/style-text.js';
|
|
11
11
|
import { Logger } from '../logger.js';
|
|
12
|
+
import { getOwnerIdFromOrgIdOrName } from './ids-resolver.js';
|
|
12
13
|
import { checkMembersToLink } from './ng-resources.js';
|
|
13
|
-
import * as Organisation from './organisation.js';
|
|
14
14
|
import { sendToApi } from './send-to-api.js';
|
|
15
|
-
import * as User from './user.js';
|
|
16
15
|
|
|
17
16
|
export const POLLING_TIMEOUT_MS = 30_000;
|
|
18
17
|
export const POLLING_INTERVAL_MS = 1000;
|
|
@@ -38,7 +37,7 @@ export const NG_MEMBER_PREFIXES = {
|
|
|
38
37
|
*/
|
|
39
38
|
export async function create(label, description, tags, membersIds, orgaIdOrName) {
|
|
40
39
|
const id = `ng_${crypto.randomUUID()}`;
|
|
41
|
-
const ownerId = await
|
|
40
|
+
const ownerId = await getOwnerIdFromOrgIdOrName(orgaIdOrName);
|
|
42
41
|
|
|
43
42
|
if (membersIds?.length > 0) {
|
|
44
43
|
checkMembersToLink(membersIds);
|
|
@@ -126,7 +125,7 @@ export async function getPeerConfig(peerIdOrLabel, ngIdOrLabel, orgaIdOrName) {
|
|
|
126
125
|
* @returns {Promise<Array<Object>>} The Network Groups
|
|
127
126
|
*/
|
|
128
127
|
export async function getNG(networkGroupId, orgaIdOrName) {
|
|
129
|
-
const ownerId = await
|
|
128
|
+
const ownerId = await getOwnerIdFromOrgIdOrName(orgaIdOrName);
|
|
130
129
|
|
|
131
130
|
Logger.info(`Get Network Group ${networkGroupId} for owner ${ownerId}`);
|
|
132
131
|
const result = await getNetworkGroup({ networkGroupId, ownerId }).then(sendToApi);
|
|
@@ -141,7 +140,7 @@ export async function getNG(networkGroupId, orgaIdOrName) {
|
|
|
141
140
|
* @returns {Promise<Array<Object>>} The Network Groups
|
|
142
141
|
*/
|
|
143
142
|
export async function getAllNGs(orgaIdOrName) {
|
|
144
|
-
const ownerId = await
|
|
143
|
+
const ownerId = await getOwnerIdFromOrgIdOrName(orgaIdOrName);
|
|
145
144
|
|
|
146
145
|
Logger.info(`Listing Network Groups from owner ${ownerId}`);
|
|
147
146
|
const result = await listNetworkGroups({ ownerId }).then(sendToApi);
|
|
@@ -159,7 +158,7 @@ export async function getAllNGs(orgaIdOrName) {
|
|
|
159
158
|
* @returns {Promise<Object>} Found results
|
|
160
159
|
*/
|
|
161
160
|
export async function searchNgOrResource(idOrLabel, orgaIdOrName, type = 'all', exactMatch = true) {
|
|
162
|
-
const ownerId = await
|
|
161
|
+
const ownerId = await getOwnerIdFromOrgIdOrName(orgaIdOrName);
|
|
163
162
|
|
|
164
163
|
// If idOrLabel is a string we use it, or we look through multiple keys
|
|
165
164
|
const query =
|
|
@@ -265,12 +264,3 @@ async function pollNetworkGroup(ownerId, ngId, { waitForMembers = null, waitForD
|
|
|
265
264
|
pollOnce();
|
|
266
265
|
});
|
|
267
266
|
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Get the owner ID from an Organisation ID or name
|
|
271
|
-
* @param {object} orgaIdOrName The Organisation ID or name
|
|
272
|
-
* @returns {Promise<string>} The owner ID
|
|
273
|
-
*/
|
|
274
|
-
async function getOwnerIdFromOrgaIdOrName(orgaIdOrName) {
|
|
275
|
-
return orgaIdOrName != null ? Organisation.getId(orgaIdOrName) : User.getCurrentId();
|
|
276
|
-
}
|
package/src/parsers.js
CHANGED
|
@@ -100,12 +100,13 @@ export function orgaIdOrName(string) {
|
|
|
100
100
|
const addonIdRegex = /^addon_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
101
101
|
const operatorIdRegex =
|
|
102
102
|
/^(keycloak|otoroshi|matomo|metabase)_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
103
|
+
const ulidRegex = /^(kubernetes)_[0-9A-HJ-NP-TV-Z]{26}$/i;
|
|
103
104
|
|
|
104
105
|
export function addonIdOrName(string) {
|
|
105
106
|
if (string.match(addonIdRegex)) {
|
|
106
107
|
return cliparse.parsers.success({ addon_id: string });
|
|
107
108
|
}
|
|
108
|
-
if (string.match(operatorIdRegex)) {
|
|
109
|
+
if (string.match(operatorIdRegex) || string.match(ulidRegex)) {
|
|
109
110
|
return cliparse.parsers.success({ operator_id: string });
|
|
110
111
|
}
|
|
111
112
|
return cliparse.parsers.success({ addon_name: string });
|