clever-tools 4.2.0 → 4.4.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/README.md +9 -9
- package/bin/clever.js +116 -14
- package/package.json +3 -2
- package/src/clever-client/drains.js +127 -0
- package/src/clever-client/k8s.js +96 -0
- package/src/clever-client/operators.js +16 -0
- package/src/commands/drain.js +118 -62
- package/src/commands/k8s.js +193 -0
- package/src/commands/otoroshi.js +12 -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/operator-commands.js +8 -0
- package/src/lib/prompts.js +2 -0
- package/src/models/drain.js +28 -132
- package/src/models/ids-resolver.js +11 -0
- package/src/models/ng.js +5 -15
- package/src/parsers.js +2 -1
package/src/commands/drain.js
CHANGED
|
@@ -1,108 +1,164 @@
|
|
|
1
|
-
import { createDrain, deleteDrain,
|
|
1
|
+
import { createDrain, deleteDrain, disableDrain, enableDrain, getDrain, getDrains } from '../clever-client/drains.js';
|
|
2
|
+
import { styleText } from '../lib/style-text.js';
|
|
2
3
|
import { Logger } from '../logger.js';
|
|
3
4
|
import * as Application from '../models/application.js';
|
|
4
|
-
import {
|
|
5
|
+
import { DRAIN_TYPE_CLI_CODES, DRAIN_TYPES, formatDrain } from '../models/drain.js';
|
|
5
6
|
import { sendToApi } from '../models/send-to-api.js';
|
|
6
7
|
|
|
7
|
-
// TODO: This could be useful in other commands
|
|
8
|
-
async function getAppOrAddonId({ alias, appIdOrName, addonId }) {
|
|
9
|
-
return addonId != null ? addonId : await Application.resolveId(appIdOrName, alias).then(({ appId }) => appId);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
8
|
export async function list(params) {
|
|
13
|
-
const { alias, app: appIdOrName,
|
|
9
|
+
const { alias, app: appIdOrName, format } = params.options;
|
|
10
|
+
|
|
11
|
+
const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
|
|
14
12
|
|
|
15
|
-
const
|
|
16
|
-
const drains = await getDrains({ appId: appIdOrAddonId }).then(sendToApi);
|
|
13
|
+
const drains = await getDrains({ ownerId, applicationId }).then(sendToApi);
|
|
17
14
|
|
|
18
15
|
switch (format) {
|
|
19
16
|
case 'json': {
|
|
20
|
-
|
|
21
|
-
id: drain.id,
|
|
22
|
-
target: drain.target,
|
|
23
|
-
state: drain.state,
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
Logger.printJson(formattedDrains);
|
|
17
|
+
Logger.printJson(drains);
|
|
27
18
|
break;
|
|
28
19
|
}
|
|
29
20
|
case 'human':
|
|
30
21
|
default: {
|
|
31
22
|
if (drains.length === 0) {
|
|
32
|
-
Logger.println(`There are no drains for ${
|
|
23
|
+
Logger.println(`There are no drains for ${applicationId}`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (drains.length === 1) {
|
|
28
|
+
const formattedDrain = formatDrain(drains[0]);
|
|
29
|
+
console.table(formattedDrain);
|
|
30
|
+
return;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
drains.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
if (structuredDataParameters != null) {
|
|
44
|
-
drainView += `, sd-params: '${structuredDataParameters}'`;
|
|
45
|
-
}
|
|
46
|
-
Logger.println(drainView);
|
|
33
|
+
const formattedDrains = drains.map((drain) => {
|
|
34
|
+
return {
|
|
35
|
+
ID: drain.id,
|
|
36
|
+
Status: drain.status.status,
|
|
37
|
+
'Execution status': drain.execution.status,
|
|
38
|
+
URL: drain.recipient.url,
|
|
39
|
+
};
|
|
47
40
|
});
|
|
41
|
+
|
|
42
|
+
console.table(formattedDrains);
|
|
48
43
|
}
|
|
49
44
|
}
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
export async function create(params) {
|
|
53
|
-
const
|
|
48
|
+
const { alias, app: appIdOrName } = params.options;
|
|
54
49
|
const {
|
|
55
|
-
alias,
|
|
56
|
-
app: appIdOrName,
|
|
57
|
-
addon: addonId,
|
|
58
50
|
username,
|
|
59
51
|
password,
|
|
60
52
|
'api-key': apiKey,
|
|
61
53
|
'index-prefix': indexPrefix,
|
|
62
|
-
'sd-params':
|
|
54
|
+
'sd-params': rfc5424StructuredDataParameters,
|
|
63
55
|
} = params.options;
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
56
|
+
const [drainTypeCliCode, url] = params.args;
|
|
57
|
+
|
|
58
|
+
const drainType = Object.values(DRAIN_TYPES).find((drainType) => drainType.cliCode === drainTypeCliCode);
|
|
59
|
+
if (!drainType) {
|
|
60
|
+
throw new Error(`Invalid drain type. Supported types are: ${DRAIN_TYPE_CLI_CODES.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
|
|
64
|
+
|
|
65
|
+
const body = {
|
|
66
|
+
kind: 'LOG',
|
|
67
|
+
recipient: {
|
|
68
|
+
type: drainType.apiCode,
|
|
69
|
+
url,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (drainTypeCliCode === DRAIN_TYPES.ELASTICSEARCH.cliCode) {
|
|
74
|
+
if (!indexPrefix) {
|
|
75
|
+
throw new Error(`${DRAIN_TYPES.ELASTICSEARCH.cliCode} drains require an index prefix (--index-prefix) to be set`);
|
|
76
|
+
}
|
|
77
|
+
if (!url.endsWith('/_bulk')) {
|
|
78
|
+
throw new Error(`${DRAIN_TYPES.ELASTICSEARCH.cliCode} drain URL must end with '/_bulk'`);
|
|
79
|
+
}
|
|
80
|
+
body.recipient.index = indexPrefix;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (drainTypeCliCode === DRAIN_TYPES.ELASTICSEARCH.cliCode || drainTypeCliCode === DRAIN_TYPES.RAW_HTTP.cliCode) {
|
|
84
|
+
if (username) {
|
|
85
|
+
body.recipient.username = username;
|
|
86
|
+
}
|
|
87
|
+
if (password) {
|
|
88
|
+
body.recipient.password = password;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (drainTypeCliCode === DRAIN_TYPES.NEWRELIC.cliCode) {
|
|
93
|
+
if (!apiKey) {
|
|
94
|
+
throw new Error(`${DRAIN_TYPES.NEWRELIC.cliCode} drains require an API key (--api-key) to be set`);
|
|
95
|
+
}
|
|
96
|
+
body.recipient.apiKey = apiKey;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
drainTypeCliCode === DRAIN_TYPES.OVH_TCP.cliCode ||
|
|
101
|
+
drainTypeCliCode === DRAIN_TYPES.SYSLOG_TCP.cliCode ||
|
|
102
|
+
drainTypeCliCode === DRAIN_TYPES.SYSLOG_UDP.cliCode
|
|
103
|
+
) {
|
|
104
|
+
if (rfc5424StructuredDataParameters) {
|
|
105
|
+
body.recipient.rfc5424StructuredDataParameters = rfc5424StructuredDataParameters;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const drain = await createDrain({ ownerId, applicationId, body }).then(sendToApi);
|
|
110
|
+
Logger.printSuccess(`Drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`);
|
|
78
111
|
}
|
|
79
112
|
|
|
80
|
-
export async function
|
|
113
|
+
export async function get(params) {
|
|
81
114
|
const [drainId] = params.args;
|
|
82
|
-
const { alias, app: appIdOrName,
|
|
115
|
+
const { alias, app: appIdOrName, format } = params.options;
|
|
116
|
+
|
|
117
|
+
const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
|
|
83
118
|
|
|
84
|
-
const
|
|
85
|
-
await deleteDrain({ appId: appIdOrAddonId, drainId }).then(sendToApi);
|
|
119
|
+
const drain = await getDrain({ ownerId, applicationId, drainId }).then(sendToApi);
|
|
86
120
|
|
|
87
|
-
|
|
121
|
+
switch (format) {
|
|
122
|
+
case 'json': {
|
|
123
|
+
Logger.printJson(drain);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'human':
|
|
127
|
+
default: {
|
|
128
|
+
const formattedDrain = formatDrain(drain);
|
|
129
|
+
console.table(formattedDrain);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function remove(params) {
|
|
135
|
+
const [drainId] = params.args;
|
|
136
|
+
const { alias, app: appIdOrName } = params.options;
|
|
137
|
+
|
|
138
|
+
const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
|
|
139
|
+
|
|
140
|
+
await deleteDrain({ ownerId, applicationId, drainId }).then(sendToApi);
|
|
141
|
+
Logger.printSuccess(`Drain ${styleText(['bold', 'green'], drainId)} has been successfully removed!`);
|
|
88
142
|
}
|
|
89
143
|
|
|
90
144
|
export async function enable(params) {
|
|
91
145
|
const [drainId] = params.args;
|
|
92
|
-
const { alias, app: appIdOrName
|
|
146
|
+
const { alias, app: appIdOrName } = params.options;
|
|
93
147
|
|
|
94
|
-
const
|
|
95
|
-
await updateDrainState({ appId: appIdOrAddonId, drainId }, { state: 'ENABLED' }).then(sendToApi);
|
|
148
|
+
const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
|
|
96
149
|
|
|
97
|
-
|
|
150
|
+
await enableDrain({ ownerId, applicationId, drainId }).then(sendToApi);
|
|
151
|
+
|
|
152
|
+
Logger.printSuccess(`Drain ${styleText(['bold', 'green'], drainId)} has been successfully enabled!`);
|
|
98
153
|
}
|
|
99
154
|
|
|
100
155
|
export async function disable(params) {
|
|
101
156
|
const [drainId] = params.args;
|
|
102
|
-
const { alias, app: appIdOrName
|
|
157
|
+
const { alias, app: appIdOrName } = params.options;
|
|
158
|
+
|
|
159
|
+
const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
|
|
103
160
|
|
|
104
|
-
|
|
105
|
-
await updateDrainState({ appId: appIdOrAddonId, drainId }, { state: 'DISABLED' }).then(sendToApi);
|
|
161
|
+
await disableDrain({ ownerId, applicationId, drainId }).then(sendToApi);
|
|
106
162
|
|
|
107
|
-
Logger.
|
|
163
|
+
Logger.printSuccess(`Drain ${styleText(['bold', 'green'], drainId)} has been successfully disabled!`);
|
|
108
164
|
}
|
|
@@ -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/otoroshi.js
CHANGED
|
@@ -50,6 +50,18 @@ export async function get(params) {
|
|
|
50
50
|
await operatorPrint('otoroshi', addonIdOrName, format);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Print the configuration of an Otoroshi operator in otoroshictl format
|
|
55
|
+
* @param {object} params The command's parameters
|
|
56
|
+
* @param {string} params.args[0] The operator's name or ID
|
|
57
|
+
* @returns {Promise<void>}
|
|
58
|
+
*/
|
|
59
|
+
export async function getConfig(params) {
|
|
60
|
+
const [addonIdOrName] = params.args;
|
|
61
|
+
const { format } = params.options;
|
|
62
|
+
await operatorPrint('otoroshi', addonIdOrName, 'otoroshictl');
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
/**
|
|
54
66
|
* List all Otoroshi operators
|
|
55
67
|
* @returns {Promise<void>}
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import _ from 'lodash';
|
|
3
3
|
import {
|
|
4
|
+
getOtoroshiConfig,
|
|
4
5
|
ngDisableOperator,
|
|
5
6
|
ngEnableOperator,
|
|
6
7
|
rebootOperator,
|
|
@@ -249,6 +250,13 @@ export async function operatorRebuild(provider, addonIdOrName) {
|
|
|
249
250
|
export async function operatorPrint(provider, addonIdOrName, format = 'human') {
|
|
250
251
|
const operator = await Operator.getDetails(provider, addonIdOrName);
|
|
251
252
|
|
|
253
|
+
if (provider === 'otoroshi' && format === 'otoroshictl') {
|
|
254
|
+
const config = await getOtoroshiConfig({ realId: operator.resourceId }).then(sendToApi);
|
|
255
|
+
|
|
256
|
+
Logger.println(config);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
252
260
|
const dataToPrint = {
|
|
253
261
|
Name: operator.name,
|
|
254
262
|
ID: operator.resourceId,
|
package/src/lib/prompts.js
CHANGED