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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clever-tools",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Command Line Interface for Clever Cloud.",
5
5
  "keywords": [
6
6
  "cli",
@@ -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:
@@ -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
+ }
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 getOwnerIdFromOrgaIdOrName(orgaIdOrName);
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 getOwnerIdFromOrgaIdOrName(orgaIdOrName);
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 getOwnerIdFromOrgaIdOrName(orgaIdOrName);
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 getOwnerIdFromOrgaIdOrName(orgaIdOrName);
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 });