clever-tools 3.11.0 → 3.13.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 +416 -3
- package/package.json +4 -3
- package/src/clever-client/auth-bridge.js +61 -0
- package/src/clever-client/ng.js +18 -0
- package/src/clever-client/operators.js +121 -0
- package/src/commands/addon.js +34 -28
- package/src/commands/cancel-deploy.js +3 -4
- package/src/commands/config.js +16 -20
- package/src/commands/console.js +4 -10
- package/src/commands/create.js +76 -9
- package/src/commands/curl.js +17 -17
- package/src/commands/database.js +9 -9
- package/src/commands/delete.js +11 -10
- package/src/commands/deploy.js +35 -21
- package/src/commands/domain.js +13 -16
- package/src/commands/emails.js +174 -0
- package/src/commands/env.js +15 -8
- package/src/commands/keycloak.js +138 -0
- package/src/commands/kv.js +1 -1
- package/src/commands/link.js +7 -3
- package/src/commands/makeDefault.js +2 -1
- package/src/commands/matomo.js +85 -0
- package/src/commands/metabase.js +112 -0
- package/src/commands/ng.js +202 -0
- package/src/commands/open.js +2 -5
- package/src/commands/otoroshi.js +138 -0
- package/src/commands/profile.js +11 -10
- package/src/commands/published-config.js +7 -7
- package/src/commands/restart.js +1 -1
- package/src/commands/ssh-keys.js +159 -0
- package/src/commands/stop.js +3 -3
- package/src/commands/tcp-redirs.js +8 -8
- package/src/commands/tokens.js +146 -0
- package/src/commands/unlink.js +2 -1
- package/src/experimental-features.js +47 -2
- package/src/lib/ng-print.js +195 -0
- package/src/lib/operator-commands.js +281 -0
- package/src/lib/prompts.js +30 -0
- package/src/lib/slugify.js +10 -0
- package/src/logger.js +3 -0
- package/src/models/activity.js +2 -3
- package/src/models/addon.js +9 -6
- package/src/models/app_configuration.js +12 -9
- package/src/models/application.js +44 -22
- package/src/models/application_configuration.js +72 -72
- package/src/models/configuration.js +1 -0
- package/src/models/git.js +29 -1
- package/src/models/ids-resolver.js +37 -1
- package/src/models/interact.js +0 -24
- package/src/models/log-v4.js +13 -4
- package/src/models/namespaces.js +3 -2
- package/src/models/ng-resources.js +270 -0
- package/src/models/ng.js +276 -0
- package/src/models/operator.js +48 -0
- package/src/models/send-to-api.js +18 -0
- package/src/models/utils.js +21 -0
- package/src/parsers.js +71 -3
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { confirm } from '../lib/prompts.js';
|
|
2
|
+
import colors from 'colors/safe.js';
|
|
3
|
+
import { Logger } from '../logger.js';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { sendToApi } from '../models/send-to-api.js';
|
|
6
|
+
import { openBrowser } from '../models/utils.js';
|
|
7
|
+
import dedent from 'dedent';
|
|
8
|
+
import {
|
|
9
|
+
todo_addSshKey as addSshKey,
|
|
10
|
+
todo_getSshKeys as getSshKeys,
|
|
11
|
+
todo_removeSshKey as removeSshKey,
|
|
12
|
+
} from '@clevercloud/client/esm/api/v2/user.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* List SSH keys of the current user
|
|
16
|
+
* @param {object} params The command parameters
|
|
17
|
+
* @param {string} params.options.format The output format
|
|
18
|
+
*/
|
|
19
|
+
export async function list (params) {
|
|
20
|
+
const { format } = params.options;
|
|
21
|
+
|
|
22
|
+
const keys = await getUserSshKeys();
|
|
23
|
+
|
|
24
|
+
switch (format) {
|
|
25
|
+
case 'json': {
|
|
26
|
+
Logger.printJson(keys);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case 'human':
|
|
30
|
+
default: {
|
|
31
|
+
if (keys.length === 0) {
|
|
32
|
+
Logger.println(dedent`
|
|
33
|
+
${colors.blue('🔐 No SSH keys')}
|
|
34
|
+
|
|
35
|
+
To list the SSH keys on your local system, use the following command:
|
|
36
|
+
${colors.grey('ssh-add -l -E sha256')}
|
|
37
|
+
|
|
38
|
+
To create a new key pair, use the following command:
|
|
39
|
+
${colors.grey('ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_clever -C "An optional comment"')}
|
|
40
|
+
|
|
41
|
+
Then add the public key to your Clever Cloud account:
|
|
42
|
+
${colors.grey('clever ssh-keys add myNewKey ~/.ssh/id_ed25519_clever.pub')}
|
|
43
|
+
`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Logger.println(`🔐 ${keys.length} SSH key(s):`);
|
|
48
|
+
keys.forEach((key) => {
|
|
49
|
+
Logger.println(` • ${colors.blue(key.name)}`, colors.grey(`(${key.fingerprint})`));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add a SSH key to the current user
|
|
57
|
+
* @param {object} params The command parameters
|
|
58
|
+
* @param {Array<string>} params.args
|
|
59
|
+
*/
|
|
60
|
+
export async function add (params) {
|
|
61
|
+
const [keyName, filePath] = params.args;
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(filePath)) {
|
|
64
|
+
throw new Error(`File ${filePath} does not exist`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const pubKeyContent = fs.readFileSync(filePath, 'utf8').trim();
|
|
68
|
+
Logger.debug(`SSH key file content: ${pubKeyContent}`);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await addSshKey({ key: encodeURIComponent(keyName) }, JSON.stringify(pubKeyContent)).then(sendToApi);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
console.log(e?.responseBody?.id);
|
|
75
|
+
if (e?.responseBody?.id === 505) {
|
|
76
|
+
throw new Error('This SSH key is not valid, please make sure you\'re pointing to the public key file');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Logger.printSuccess(`SSH key ${keyName} added successfully`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove a SSH key from the current user
|
|
85
|
+
* @param {object} params The command parameters
|
|
86
|
+
* @param {Array<string>} params.args
|
|
87
|
+
*/
|
|
88
|
+
export async function remove (params) {
|
|
89
|
+
const [keyName] = params.args;
|
|
90
|
+
|
|
91
|
+
const keys = await getUserSshKeys();
|
|
92
|
+
|
|
93
|
+
if (keys.find((key) => key.name === keyName) == null) {
|
|
94
|
+
throw new Error(`SSH key ${colors.red(keyName)} not found`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const keyNameEncoded = encodeURIComponent(keyName);
|
|
98
|
+
await removeSshKey({ key: keyNameEncoded }).then(sendToApi);
|
|
99
|
+
|
|
100
|
+
Logger.printSuccess(`SSH key ${keyName} removed successfully`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Remove all SSH keys from the current user
|
|
105
|
+
* @param {object} params The command parameters
|
|
106
|
+
* @param {object} params.options The command options
|
|
107
|
+
* @param {boolean} params.options.yes The user confirmation
|
|
108
|
+
*/
|
|
109
|
+
export async function removeAll (params) {
|
|
110
|
+
if (!params.options.yes) {
|
|
111
|
+
await confirm(
|
|
112
|
+
'Are you sure you want to remove all your SSH keys?',
|
|
113
|
+
'No SSH keys removed',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const keys = await getUserSshKeys();
|
|
118
|
+
|
|
119
|
+
if (keys.length === 0) {
|
|
120
|
+
Logger.println('No SSH keys to remove');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const results = await Promise.all(
|
|
125
|
+
keys.map((key) => {
|
|
126
|
+
const keyNameEncoded = encodeURIComponent(key.name);
|
|
127
|
+
return removeSshKey({ key: keyNameEncoded }).then(sendToApi)
|
|
128
|
+
.then(() => [true, key.name])
|
|
129
|
+
.catch(() => [false, key.name]);
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (results.every(([isRemoved]) => isRemoved)) {
|
|
134
|
+
Logger.printSuccess('All SSH keys were removed successfully');
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const keyNamesWithErrors = results
|
|
138
|
+
.filter(([isRemoved]) => !isRemoved)
|
|
139
|
+
.map(([_, keyName]) => keyName)
|
|
140
|
+
.join(', ');
|
|
141
|
+
throw new Error(`Some errors occured while removing these SSH keys: ${keyNamesWithErrors}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Open the SSH keys management page of the Console in your browser
|
|
147
|
+
* @returns {Promise<void>} A promise that resolves when the page is opened
|
|
148
|
+
*/
|
|
149
|
+
export function openConsole () {
|
|
150
|
+
return openBrowser('/users/me/ssh-keys', 'Opening the SSH keys management page of the Console in your browser');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @return {Promise<Array<{ name: string, key: string, fingerprint: string }>>}
|
|
155
|
+
*/
|
|
156
|
+
async function getUserSshKeys () {
|
|
157
|
+
const rawKeys = await getSshKeys().then(sendToApi);
|
|
158
|
+
return rawKeys.sort((a, b) => a.name.localeCompare(b.name));
|
|
159
|
+
}
|
package/src/commands/stop.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as Application from '../models/application.js';
|
|
2
|
-
import * as application from '@clevercloud/client/esm/api/v2/application.js';
|
|
3
2
|
import { Logger } from '../logger.js';
|
|
4
3
|
import { sendToApi } from '../models/send-to-api.js';
|
|
4
|
+
import { undeploy as stopApplication } from '@clevercloud/client/esm/api/v2/application.js';
|
|
5
5
|
|
|
6
6
|
export async function stop (params) {
|
|
7
7
|
const { alias, app: appIdOrName } = params.options;
|
|
8
8
|
const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
|
|
9
9
|
|
|
10
|
-
await
|
|
11
|
-
Logger.
|
|
10
|
+
await stopApplication({ id: ownerId, appId }).then(sendToApi);
|
|
11
|
+
Logger.printSuccess('Application successfully stopped!');
|
|
12
12
|
}
|
|
@@ -2,10 +2,10 @@ import colors from 'colors/safe.js';
|
|
|
2
2
|
|
|
3
3
|
import * as Namespaces from '../models/namespaces.js';
|
|
4
4
|
import { sendToApi } from '../models/send-to-api.js';
|
|
5
|
-
import
|
|
5
|
+
import { confirm } from '../lib/prompts.js';
|
|
6
6
|
import { Logger } from '../logger.js';
|
|
7
|
-
import * as application from '@clevercloud/client/esm/api/v2/application.js';
|
|
8
7
|
import * as Application from '../models/application.js';
|
|
8
|
+
import { addTcpRedir, getTcpRedirs, removeTcpRedir } from '@clevercloud/client/esm/api/v2/application.js';
|
|
9
9
|
|
|
10
10
|
export async function listNamespaces (params) {
|
|
11
11
|
const { alias, app: appIdOrName, format } = params.options;
|
|
@@ -44,7 +44,7 @@ export async function list (params) {
|
|
|
44
44
|
const { alias, app: appIdOrName, format } = params.options;
|
|
45
45
|
const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
|
|
46
46
|
|
|
47
|
-
const redirs = await
|
|
47
|
+
const redirs = await getTcpRedirs({ id: ownerId, appId }).then(sendToApi);
|
|
48
48
|
|
|
49
49
|
switch (format) {
|
|
50
50
|
case 'json': {
|
|
@@ -72,8 +72,8 @@ async function acceptPayment (result, skipConfirmation) {
|
|
|
72
72
|
Logger.println(`Total (without taxes): ${result.totalHT}€`);
|
|
73
73
|
Logger.println(colors.bold(`Total (with taxes): ${result.totalTTC}€`));
|
|
74
74
|
|
|
75
|
-
await
|
|
76
|
-
`You're about to pay ${result.totalTTC}€, confirm
|
|
75
|
+
await confirm(
|
|
76
|
+
`You're about to pay ${result.totalTTC}€, confirm?`,
|
|
77
77
|
'No confirmation, aborting TCP redirection creation',
|
|
78
78
|
);
|
|
79
79
|
}
|
|
@@ -83,10 +83,10 @@ export async function add (params) {
|
|
|
83
83
|
const { alias, app: appIdOrName, namespace, yes: skipConfirmation } = params.options;
|
|
84
84
|
const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
|
|
85
85
|
|
|
86
|
-
const { port } = await
|
|
86
|
+
const { port } = await addTcpRedir({ id: ownerId, appId }, { namespace }).then(sendToApi).catch((error) => {
|
|
87
87
|
if (error.status === 402) {
|
|
88
88
|
return acceptPayment(error.response.body, skipConfirmation).then(() => {
|
|
89
|
-
return
|
|
89
|
+
return addTcpRedir({ id: ownerId, appId, payment: 'accepted' }, { namespace }).then(sendToApi);
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
else {
|
|
@@ -102,7 +102,7 @@ export async function remove (params) {
|
|
|
102
102
|
const { alias, app: appIdOrName, namespace } = params.options;
|
|
103
103
|
const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
|
|
104
104
|
|
|
105
|
-
await
|
|
105
|
+
await removeTcpRedir({ id: ownerId, appId, sourcePort: port, namespace }).then(sendToApi);
|
|
106
106
|
|
|
107
107
|
Logger.println('Successfully removed tcp redirection.');
|
|
108
108
|
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import colors from 'colors/safe.js';
|
|
2
|
+
import { Logger } from '../logger.js';
|
|
3
|
+
import { createApiToken, deleteApiToken, listApiTokens } from '../clever-client/auth-bridge.js';
|
|
4
|
+
import { sendToAuthBridge } from '../models/send-to-api.js';
|
|
5
|
+
import { getCurrent as getCurrentUser } from '../models/user.js';
|
|
6
|
+
import { conf } from '../models/configuration.js';
|
|
7
|
+
import dedent from 'dedent';
|
|
8
|
+
import { promptSecret } from '../lib/prompts.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a new API token
|
|
12
|
+
* @param {Object} params - Function parameters
|
|
13
|
+
* @param {[string]} params.args - Command line args
|
|
14
|
+
* @param {Object} params.options - Command line options
|
|
15
|
+
* @param {'json'|'human'} params.options.format - Output format
|
|
16
|
+
* @param {number} params.options.expiration - Expiration date as timestamp
|
|
17
|
+
*/
|
|
18
|
+
export async function create (params) {
|
|
19
|
+
const [apiTokenName] = params.args;
|
|
20
|
+
const { expiration, format } = params.options;
|
|
21
|
+
const user = await getCurrentUser();
|
|
22
|
+
|
|
23
|
+
if (!user.hasPassword) {
|
|
24
|
+
const apiTokenListHref = new URL('/users/me/api-tokens', conf.CONSOLE_URL).href;
|
|
25
|
+
throw new Error(dedent`
|
|
26
|
+
${colors.yellow('!')} Your Clever Cloud account is linked via GitHub and has no password. Setting one is required to create API tokens.
|
|
27
|
+
${colors.blue('→')} To do so, go to the following URL: ${colors.blue(apiTokenListHref)}
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Expire in 1 year
|
|
32
|
+
const dateObject = new Date();
|
|
33
|
+
dateObject.setFullYear(dateObject.getFullYear() + 1);
|
|
34
|
+
const maxExpirationDate = dateObject;
|
|
35
|
+
|
|
36
|
+
let expirationDate;
|
|
37
|
+
if (expiration != null) {
|
|
38
|
+
if (expiration > maxExpirationDate.getTime()) {
|
|
39
|
+
throw new Error('You cannot set an expiration date greater than 1 year');
|
|
40
|
+
}
|
|
41
|
+
expirationDate = new Date(expiration);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
expirationDate = maxExpirationDate;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const password = await promptSecret('Enter your password:');
|
|
48
|
+
|
|
49
|
+
let mfaCode;
|
|
50
|
+
if (user.preferredMFA === 'TOTP') {
|
|
51
|
+
mfaCode = await promptSecret('Enter your 2FA code:');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tokenData = {
|
|
55
|
+
email: user.email,
|
|
56
|
+
password,
|
|
57
|
+
mfaCode,
|
|
58
|
+
name: apiTokenName,
|
|
59
|
+
expirationDate: expirationDate.toISOString(),
|
|
60
|
+
};
|
|
61
|
+
const createdToken = await createApiToken(tokenData).then(sendToAuthBridge).catch((error) => {
|
|
62
|
+
const errorCode = error?.cause?.responseBody?.code;
|
|
63
|
+
if (errorCode === 'invalid-credential') {
|
|
64
|
+
throw new Error('Invalid credentials, check your password');
|
|
65
|
+
}
|
|
66
|
+
if (errorCode === 'invalid-mfa-code') {
|
|
67
|
+
throw new Error('Invalid credentials, check your 2FA code');
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
switch (format) {
|
|
73
|
+
case 'json':
|
|
74
|
+
Logger.printJson(createdToken);
|
|
75
|
+
break;
|
|
76
|
+
case 'human':
|
|
77
|
+
default:
|
|
78
|
+
Logger.println(dedent`
|
|
79
|
+
${colors.green('✔')} API token successfully created! Store it securely, you won't able to print it again.
|
|
80
|
+
|
|
81
|
+
- API token ID : ${colors.grey(createdToken.apiTokenId)}
|
|
82
|
+
- API token : ${colors.grey(createdToken.apiToken)}
|
|
83
|
+
- Expiration : ${colors.grey(formatDate(createdToken.expirationDate))}
|
|
84
|
+
|
|
85
|
+
Export this token and use it to make authenticated requests to the Clever Cloud API through the Auth Bridge:
|
|
86
|
+
|
|
87
|
+
export CC_API_TOKEN=${createdToken.apiToken}
|
|
88
|
+
curl -H "Authorization: Bearer $CC_API_TOKEN" ${conf.AUTH_BRIDGE_HOST}/v2/self
|
|
89
|
+
|
|
90
|
+
Then, to revoke this token, run:
|
|
91
|
+
clever tokens revoke ${createdToken.apiTokenId}
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get information about an API token
|
|
98
|
+
* @param {Object} params - Function parameters
|
|
99
|
+
* @param {Object} params.options - Command line options
|
|
100
|
+
* @param {Object} params.options.format - Output format
|
|
101
|
+
* @returns {Promise<void>}
|
|
102
|
+
*/
|
|
103
|
+
export async function list (params) {
|
|
104
|
+
const { format } = params.options;
|
|
105
|
+
|
|
106
|
+
const tokens = await listApiTokens().then(sendToAuthBridge);
|
|
107
|
+
|
|
108
|
+
if (format === 'json') {
|
|
109
|
+
Logger.printJson(tokens);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
if (tokens.length === 0) {
|
|
113
|
+
Logger.println(`ℹ️ No API token found, create one with ${colors.blue('clever tokens create')} command`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.table(tokens.map((token) => {
|
|
117
|
+
return {
|
|
118
|
+
'API token ID': token.apiTokenId,
|
|
119
|
+
Name: token.name,
|
|
120
|
+
'Creation IP address': token.ip,
|
|
121
|
+
Creation: formatDate(token.creationDate),
|
|
122
|
+
Expiration: formatDate(token.expirationDate),
|
|
123
|
+
State: token.state,
|
|
124
|
+
};
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Revoke an API token
|
|
132
|
+
* @param {Object} params - Function parameters
|
|
133
|
+
* @param {string[]} params.args - Command line arguments, token ID to revoke is expected as first argument
|
|
134
|
+
* @returns {Promise<void>}
|
|
135
|
+
*/
|
|
136
|
+
export async function revoke (params) {
|
|
137
|
+
const [apiTokenId] = params.args;
|
|
138
|
+
|
|
139
|
+
await deleteApiToken(apiTokenId).then(sendToAuthBridge);
|
|
140
|
+
|
|
141
|
+
Logger.println(colors.green('✔'), 'API token successfully revoked!');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatDate (dateInput) {
|
|
145
|
+
return new Date(dateInput).toISOString().substring(0, 16).replace('T', ' ');
|
|
146
|
+
}
|
package/src/commands/unlink.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as AppConfig from '../models/app_configuration.js';
|
|
2
2
|
import * as Application from '../models/application.js';
|
|
3
3
|
import { Logger } from '../logger.js';
|
|
4
|
+
import colors from 'colors/safe.js';
|
|
4
5
|
|
|
5
6
|
export async function unlink (params) {
|
|
6
7
|
const [alias] = params.args;
|
|
7
8
|
const app = await AppConfig.getAppDetails({ alias });
|
|
8
9
|
|
|
9
10
|
await Application.unlinkRepo(app.alias);
|
|
10
|
-
Logger.
|
|
11
|
+
Logger.printSuccess(`Application ${colors.green(app.appId)} has been successfully unlinked from local alias ${colors.green(app.alias)}!`);
|
|
11
12
|
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import dedent from 'dedent';
|
|
2
|
+
|
|
1
3
|
export const EXPERIMENTAL_FEATURES = {
|
|
2
4
|
kv: {
|
|
3
5
|
status: 'alpha',
|
|
4
6
|
description: 'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
|
|
5
|
-
instructions: `
|
|
7
|
+
instructions: dedent`
|
|
6
8
|
Target any compatible add-on by its name or ID (with an org ID if needed) and send commands to it:
|
|
7
9
|
|
|
8
10
|
clever kv myMateriaKV SET myKey myValue
|
|
@@ -12,6 +14,49 @@ export const EXPERIMENTAL_FEATURES = {
|
|
|
12
14
|
clever kv redis_xxxxx --org org_xxxxx PING
|
|
13
15
|
|
|
14
16
|
Learn more about Materia KV: https://www.clever-cloud.com/developers/doc/addons/materia-kv/
|
|
15
|
-
|
|
17
|
+
`,
|
|
18
|
+
},
|
|
19
|
+
ng: {
|
|
20
|
+
status: 'beta',
|
|
21
|
+
description: 'Manage Network Groups to manage applications, add-ons, external peers through a Wireguard network',
|
|
22
|
+
instructions: dedent`
|
|
23
|
+
- Create a Network Group:
|
|
24
|
+
clever ng create myNG
|
|
25
|
+
- Create a Network Group with members (application, database add-on):
|
|
26
|
+
clever ng create myNG --link app_xxx,addon_xxx
|
|
27
|
+
- List Network Groups:
|
|
28
|
+
clever ng
|
|
29
|
+
- Delete a Network Group:
|
|
30
|
+
clever ng delete myNG
|
|
31
|
+
- (Un)Link an application or a database add-on to an existing Network Group:
|
|
32
|
+
clever ng link app_xxx myNG
|
|
33
|
+
clever ng unlink addon_xxx myNG
|
|
34
|
+
- Get the Wireguard configuration of a peer:
|
|
35
|
+
clever ng get-config peerIdOrLabel myNG
|
|
36
|
+
- Get details about a Network Group, a member or a peer:
|
|
37
|
+
clever ng get myNg
|
|
38
|
+
clever ng get app_xxx
|
|
39
|
+
clever ng get peerId
|
|
40
|
+
clever ng get memberLabel
|
|
41
|
+
- Search Network Groups, members or peers:
|
|
42
|
+
clever ng search myQuery
|
|
43
|
+
|
|
44
|
+
Learn more about Network Groups: https://www.clever-cloud.com/developers/doc/develop/network-groups/
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
operators: {
|
|
48
|
+
status: 'beta',
|
|
49
|
+
description: 'Manage operators and their features such as Keycloak, Matomo, Metabase, Otoroshi',
|
|
50
|
+
instructions: dedent`
|
|
51
|
+
clever keycloak
|
|
52
|
+
clever keycloak get keycloak_xxx
|
|
53
|
+
clever keycloak ng enable myKeycloak
|
|
54
|
+
|
|
55
|
+
clever metabase version check myMetabase
|
|
56
|
+
clever metabase version update myMetabase 0.53
|
|
57
|
+
|
|
58
|
+
clever matomo open myMatomo
|
|
59
|
+
clever otoroshi open logs myOtoroshi
|
|
60
|
+
`,
|
|
16
61
|
},
|
|
17
62
|
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import colors from 'colors/safe.js';
|
|
2
|
+
import * as networkGroup from '../models/ng.js';
|
|
3
|
+
|
|
4
|
+
import { Logger } from '../logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Print a Network Group
|
|
8
|
+
* @param {Object} ng The Network Group to print
|
|
9
|
+
* @param {string} format Output format
|
|
10
|
+
* @param {boolean} full If true, get more details about the Network Group (default: false)
|
|
11
|
+
*/
|
|
12
|
+
function printNg (ng, format, full = false) {
|
|
13
|
+
|
|
14
|
+
switch (format) {
|
|
15
|
+
case 'json': {
|
|
16
|
+
Logger.printJson(ng);
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case 'human':
|
|
20
|
+
default: {
|
|
21
|
+
const ngData = {
|
|
22
|
+
ID: ng.id,
|
|
23
|
+
Label: ng.label,
|
|
24
|
+
Description: ng.description,
|
|
25
|
+
Network: `${ng.networkIp}`,
|
|
26
|
+
'Members/Peers': `${Object.keys(ng.members)?.length}/${Object.keys(ng.peers)?.length}`,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
console.table(ngData);
|
|
30
|
+
|
|
31
|
+
if (full) {
|
|
32
|
+
const members = Object.entries(ng.members)
|
|
33
|
+
.sort((a, b) => a[1].domainName.localeCompare(b[1].domainName))
|
|
34
|
+
.map(([id, member]) => ({
|
|
35
|
+
Domain: member.domainName,
|
|
36
|
+
}));
|
|
37
|
+
if (members.length > 0) {
|
|
38
|
+
Logger.println(colors.bold(' • Members:'));
|
|
39
|
+
console.table(members);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const peers = Object.entries(ng.peers)
|
|
43
|
+
.sort((a, b) => a[1].parentMember.localeCompare(b[1].parentMember))
|
|
44
|
+
.map(([id, peer]) => formatPeer(peer));
|
|
45
|
+
if (peers.length > 0) {
|
|
46
|
+
Logger.println(colors.bold(' • Peers:'));
|
|
47
|
+
console.table(peers);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Print a Network Group member
|
|
56
|
+
* @param {Object} member The Network Group member to print
|
|
57
|
+
* @param {string} format Output format
|
|
58
|
+
*/
|
|
59
|
+
function printMember (member, format) {
|
|
60
|
+
|
|
61
|
+
switch (format) {
|
|
62
|
+
case 'json': {
|
|
63
|
+
Logger.printJson(member);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'human':
|
|
67
|
+
default: {
|
|
68
|
+
console.table({
|
|
69
|
+
Label: member.label,
|
|
70
|
+
Domain: member.domainName,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Print a Network Group peer
|
|
78
|
+
* @param {Object} peer The Network Group peer to print
|
|
79
|
+
* @param {string} format Output format
|
|
80
|
+
* @param {boolean} full If true, get more details about the peer (default: false)
|
|
81
|
+
*/
|
|
82
|
+
function printPeer (peer, format, full = false) {
|
|
83
|
+
switch (format) {
|
|
84
|
+
case 'json': {
|
|
85
|
+
Logger.printJson(peer);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case 'human':
|
|
89
|
+
default: {
|
|
90
|
+
console.table(formatPeer(peer, full));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format a peer to print
|
|
97
|
+
* @param {Object} peer
|
|
98
|
+
* @param {boolean} full If true, get more details about the peer (default: false)
|
|
99
|
+
*/
|
|
100
|
+
function formatPeer (peer, full = false) {
|
|
101
|
+
const peerToPrint = {
|
|
102
|
+
'Parent Member': peer.parentMember,
|
|
103
|
+
ID: peer.id,
|
|
104
|
+
Label: peer.label,
|
|
105
|
+
Type: peer.type,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (full) {
|
|
109
|
+
if (peer.endpoint.ngTerm != null) {
|
|
110
|
+
peerToPrint['Host:IP'] = `${peer.endpoint.ngTerm.host}:${peer.endpoint.ngTerm.port}`;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
peerToPrint.Host = peer.endpoint.ngIp;
|
|
114
|
+
}
|
|
115
|
+
if (peer.endpoint.publicTerm != null) {
|
|
116
|
+
peerToPrint['Public Term'] = `${peer.endpoint.publicTerm.host}:${peer.endpoint.publicTerm.port}`;
|
|
117
|
+
}
|
|
118
|
+
peerToPrint['Public Key'] = peer.publicKey;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return peerToPrint;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Print the results of a search or get action
|
|
125
|
+
* @param {object} idOrLabel ID or label of the Network Group, a member or a peer
|
|
126
|
+
* @param {object} org Organisation ID or name
|
|
127
|
+
* @param {string} format Output format
|
|
128
|
+
* @param {string} action Action to perform (search or get)
|
|
129
|
+
* @param {string} type Type of item to search (NetworkGroup, Member, Peer)
|
|
130
|
+
*/
|
|
131
|
+
export async function printResults (idOrLabel, org, format, action, type) {
|
|
132
|
+
|
|
133
|
+
const exactMatch = action === 'get';
|
|
134
|
+
const toLookFor = type ?? (action === 'search' ? 'all' : 'single');
|
|
135
|
+
|
|
136
|
+
const found = await networkGroup.searchNgOrResource(idOrLabel, org, toLookFor, exactMatch);
|
|
137
|
+
|
|
138
|
+
if (!found.length) {
|
|
139
|
+
const searchString = idOrLabel.ngId
|
|
140
|
+
?? idOrLabel.memberId
|
|
141
|
+
?? idOrLabel.ngResourceLabel;
|
|
142
|
+
Logger.println(`${colors.blue('!')} No Network Group or resource found for ${colors.blue(searchString)}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (found.length === 1) {
|
|
147
|
+
switch (found[0].type) {
|
|
148
|
+
case 'NetworkGroup':
|
|
149
|
+
return printNg(found[0], format, true);
|
|
150
|
+
case 'Member':
|
|
151
|
+
return printMember(found[0], format);
|
|
152
|
+
case 'CleverPeer':
|
|
153
|
+
case 'ExternalPeer':
|
|
154
|
+
return printPeer(found[0], format, true);
|
|
155
|
+
default:
|
|
156
|
+
throw new Error(`Unknown item type: ${found[0].type}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (action === 'search') {
|
|
161
|
+
// Group found items by type in a new object
|
|
162
|
+
const grouped = found.reduce((acc, item) => {
|
|
163
|
+
if (!acc[item.type]) {
|
|
164
|
+
acc[item.type] = [];
|
|
165
|
+
}
|
|
166
|
+
acc[item.type].push(item);
|
|
167
|
+
return acc;
|
|
168
|
+
}, {});
|
|
169
|
+
|
|
170
|
+
switch (format) {
|
|
171
|
+
case 'json': {
|
|
172
|
+
Logger.printJson(grouped);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case 'human':
|
|
176
|
+
default: {
|
|
177
|
+
if (grouped.NetworkGroup) {
|
|
178
|
+
Logger.println(`${colors.bold(` • Found ${grouped.NetworkGroup.length} Network Group(s):`)}`);
|
|
179
|
+
grouped.NetworkGroup?.forEach((item) => printNg(item, format));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (grouped.Member) {
|
|
183
|
+
Logger.println(`${colors.bold(` • Found ${grouped.Member.length} Member(s):`)}`);
|
|
184
|
+
grouped.Member?.forEach((item) => printMember(item, format));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (grouped.ExternalPeer || grouped.CleverPeer) {
|
|
188
|
+
Logger.println(`${colors.bold(` • Found ${grouped.ExternalPeer.length + grouped.CleverPeer.length} Peer(s):`)}`);
|
|
189
|
+
grouped.CleverPeer?.forEach((item) => printPeer(item, format));
|
|
190
|
+
grouped.ExternalPeer?.forEach((item) => printPeer(item, format));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|