clever-tools 3.10.1 → 3.11.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 +1 -1
- package/bin/clever.js +67 -5
- package/package.json +2 -1
- package/src/commands/addon.js +24 -6
- package/src/commands/console.js +3 -4
- package/src/commands/curl.js +4 -4
- package/src/commands/features.js +93 -0
- package/src/commands/kv.js +93 -0
- package/src/commands/profile.js +7 -1
- package/src/experimental-features.js +17 -0
- package/src/models/configuration.js +50 -15
- package/src/models/ids-resolver.js +38 -1
- package/src/models/send-to-api.js +1 -8
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Discover how to use Clever Tools through [our documentation](docs/).
|
|
|
56
56
|
|
|
57
57
|
## Examples
|
|
58
58
|
|
|
59
|
-
Discover how to deploy many applications on Clever Cloud within [our guides](https://
|
|
59
|
+
Discover how to deploy many applications on Clever Cloud within [our guides](https://www.clever-cloud.com/developers/guides/).
|
|
60
60
|
|
|
61
61
|
## How to send feedback?
|
|
62
62
|
|
package/bin/clever.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import '../src/initial-setup.js';
|
|
5
5
|
|
|
6
6
|
import cliparse from 'cliparse';
|
|
7
|
+
import colors from 'colors/safe.js';
|
|
7
8
|
import cliparseCommands from 'cliparse/src/command.js';
|
|
8
9
|
import _sortBy from 'lodash/sortBy.js';
|
|
9
10
|
|
|
@@ -13,7 +14,9 @@ import * as Parsers from '../src/parsers.js';
|
|
|
13
14
|
import { handleCommandPromise } from '../src/command-promise-handler.js';
|
|
14
15
|
import * as Application from '../src/models/application.js';
|
|
15
16
|
import { AVAILABLE_ZONES } from '../src/models/application.js';
|
|
17
|
+
import { EXPERIMENTAL_FEATURES } from '../src/experimental-features.js';
|
|
16
18
|
import { getExitOnOption, getOutputFormatOption, getSameCommitPolicyOption } from '../src/command-options.js';
|
|
19
|
+
import { getFeatures } from '../src/models/configuration.js';
|
|
17
20
|
|
|
18
21
|
import * as Addon from '../src/models/addon.js';
|
|
19
22
|
import * as ApplicationConfiguration from '../src/models/application_configuration.js';
|
|
@@ -34,6 +37,8 @@ import * as diag from '../src/commands/diag.js';
|
|
|
34
37
|
import * as domain from '../src/commands/domain.js';
|
|
35
38
|
import * as drain from '../src/commands/drain.js';
|
|
36
39
|
import * as env from '../src/commands/env.js';
|
|
40
|
+
import * as features from '../src/commands/features.js';
|
|
41
|
+
import * as kv from '../src/commands/kv.js';
|
|
37
42
|
import * as link from '../src/commands/link.js';
|
|
38
43
|
import * as login from '../src/commands/login.js';
|
|
39
44
|
import * as logout from '../src/commands/logout.js';
|
|
@@ -74,10 +79,21 @@ cliparse.command = function (name, options, commandFunction) {
|
|
|
74
79
|
});
|
|
75
80
|
};
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
// Add a yellow color and status tag to the description of an experimental command
|
|
83
|
+
function colorizeExperimentalCommand (command, id) {
|
|
84
|
+
const status = EXPERIMENTAL_FEATURES[id].status;
|
|
85
|
+
command.description = colors.yellow(command.description + ' [' + status.toUpperCase() + ']');
|
|
86
|
+
return command;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function run () {
|
|
78
90
|
|
|
79
91
|
// ARGUMENTS
|
|
80
92
|
const args = {
|
|
93
|
+
kvRawCommand: cliparse.argument('command', { description: 'The raw command to send to the Materia KV or Redis® add-on' }),
|
|
94
|
+
kvIdOrName: cliparse.argument('kv-id', {
|
|
95
|
+
description: 'Add-on/Real ID (or name, if unambiguous) of a Materia KV or Redis® add-on',
|
|
96
|
+
}),
|
|
81
97
|
addonIdOrName: cliparse.argument('addon-id', {
|
|
82
98
|
description: 'Add-on ID (or name, if unambiguous)',
|
|
83
99
|
parser: Parsers.addonIdOrName,
|
|
@@ -102,6 +118,11 @@ function run () {
|
|
|
102
118
|
}),
|
|
103
119
|
drainUrl: cliparse.argument('drain-url', { description: 'Drain URL' }),
|
|
104
120
|
fqdn: cliparse.argument('fqdn', { description: 'Domain name of the application' }),
|
|
121
|
+
features: cliparse.argument('features', {
|
|
122
|
+
description: 'Comma-separated list of experimental features to manage',
|
|
123
|
+
parser: Parsers.commaSeparated,
|
|
124
|
+
}),
|
|
125
|
+
featureId: cliparse.argument('feature', { description: 'Experimental feature to manage' }),
|
|
105
126
|
notificationName: cliparse.argument('name', { description: 'Notification name' }),
|
|
106
127
|
notificationId: cliparse.argument('notification-id', { description: 'Notification ID' }),
|
|
107
128
|
webhookUrl: cliparse.argument('url', { description: 'Webhook URL' }),
|
|
@@ -666,6 +687,35 @@ function run () {
|
|
|
666
687
|
commands: [envSetCommand, envRemoveCommand, envImportCommand, envImportVarsFromLocalEnvCommand],
|
|
667
688
|
}, env.list);
|
|
668
689
|
|
|
690
|
+
// EXPERIMENTAL FEATURES COMMAND
|
|
691
|
+
const listFeaturesCommand = cliparse.command('list', {
|
|
692
|
+
description: 'List available experimental features',
|
|
693
|
+
options: [opts.humanJsonOutputFormat],
|
|
694
|
+
}, features.list);
|
|
695
|
+
const infoFeaturesCommand = cliparse.command('info', {
|
|
696
|
+
description: 'Display info about an experimental feature',
|
|
697
|
+
args: [args.featureId],
|
|
698
|
+
}, features.info);
|
|
699
|
+
const enableFeatureCommand = cliparse.command('enable', {
|
|
700
|
+
description: 'Enable experimental features',
|
|
701
|
+
args: [args.features],
|
|
702
|
+
}, features.enable);
|
|
703
|
+
const disableFeatureCommand = cliparse.command('disable', {
|
|
704
|
+
description: 'Disable experimental features',
|
|
705
|
+
args: [args.features],
|
|
706
|
+
}, features.disable);
|
|
707
|
+
const featuresCommands = cliparse.command('features', {
|
|
708
|
+
description: 'Manage Clever Tools experimental features',
|
|
709
|
+
commands: [enableFeatureCommand, disableFeatureCommand, listFeaturesCommand, infoFeaturesCommand],
|
|
710
|
+
}, features.list);
|
|
711
|
+
|
|
712
|
+
// KV COMMAND
|
|
713
|
+
const kvRawCommand = cliparse.command('kv', {
|
|
714
|
+
description: 'Send a raw command to a Materia KV or Redis® add-on',
|
|
715
|
+
args: [args.kvIdOrName, args.kvRawCommand],
|
|
716
|
+
options: [opts.orgaIdOrName, opts.humanJsonOutputFormat],
|
|
717
|
+
}, kv.sendRawCommand);
|
|
718
|
+
|
|
669
719
|
// LINK COMMAND
|
|
670
720
|
const appLinkCommand = cliparse.command('link', {
|
|
671
721
|
description: 'Link this repo to an existing application',
|
|
@@ -715,7 +765,7 @@ function run () {
|
|
|
715
765
|
|
|
716
766
|
// OPEN COMMAND
|
|
717
767
|
const openCommand = cliparse.command('open', {
|
|
718
|
-
description: 'Open an application in
|
|
768
|
+
description: 'Open an application in the Console',
|
|
719
769
|
options: [opts.alias, opts.appIdOrName],
|
|
720
770
|
}, open.open);
|
|
721
771
|
|
|
@@ -726,9 +776,13 @@ function run () {
|
|
|
726
776
|
}, consoleModule.openConsole);
|
|
727
777
|
|
|
728
778
|
// PROFILE COMMAND
|
|
779
|
+
const profileOpenCommand = cliparse.command('open', {
|
|
780
|
+
description: 'Open your profile in the Console',
|
|
781
|
+
}, profile.openProfile);
|
|
729
782
|
const profileCommand = cliparse.command('profile', {
|
|
730
783
|
description: 'Display the profile of the current user',
|
|
731
784
|
options: [opts.humanJsonOutputFormat],
|
|
785
|
+
commands: [profileOpenCommand],
|
|
732
786
|
}, profile.profile);
|
|
733
787
|
|
|
734
788
|
// PUBLISHED CONFIG COMMANDS
|
|
@@ -881,7 +935,7 @@ function run () {
|
|
|
881
935
|
// Patch help command description
|
|
882
936
|
cliparseCommands.helpCommand.description = 'Display help about the Clever Cloud CLI';
|
|
883
937
|
|
|
884
|
-
const commands =
|
|
938
|
+
const commands = [
|
|
885
939
|
accesslogsCommand,
|
|
886
940
|
activityCommand,
|
|
887
941
|
addonCommands,
|
|
@@ -900,6 +954,7 @@ function run () {
|
|
|
900
954
|
drainCommands,
|
|
901
955
|
emailNotificationsCommand,
|
|
902
956
|
envCommands,
|
|
957
|
+
featuresCommands,
|
|
903
958
|
cliparseCommands.helpCommand,
|
|
904
959
|
loginCommand,
|
|
905
960
|
logoutCommand,
|
|
@@ -918,7 +973,14 @@ function run () {
|
|
|
918
973
|
tcpRedirsCommands,
|
|
919
974
|
versionCommand,
|
|
920
975
|
webhooksCommand,
|
|
921
|
-
]
|
|
976
|
+
];
|
|
977
|
+
|
|
978
|
+
// Add experimental features only if they are enabled through the configuration file
|
|
979
|
+
const featuresFromConf = await getFeatures();
|
|
980
|
+
|
|
981
|
+
if (featuresFromConf.kv) {
|
|
982
|
+
commands.push(colorizeExperimentalCommand(kvRawCommand, 'kv'));
|
|
983
|
+
}
|
|
922
984
|
|
|
923
985
|
// CLI PARSER
|
|
924
986
|
const cliParser = cliparse.cli({
|
|
@@ -927,7 +989,7 @@ function run () {
|
|
|
927
989
|
version: getPackageJson().version,
|
|
928
990
|
options: [opts.color, opts.updateNotifier, opts.verbose],
|
|
929
991
|
helpCommand: false,
|
|
930
|
-
commands,
|
|
992
|
+
commands: _sortBy(commands, 'name'),
|
|
931
993
|
});
|
|
932
994
|
|
|
933
995
|
// Make sure argv[0] is always "node"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clever-tools",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"description": "Command Line Interface for Clever Cloud.",
|
|
5
5
|
"main": "bin/clever.js",
|
|
6
6
|
"type": "module",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"curlconverter": "3.21.0",
|
|
34
34
|
"duration-js": "4.0.0",
|
|
35
35
|
"eventsource": "1.1.0",
|
|
36
|
+
"ioredis": "5.4.1",
|
|
36
37
|
"iso8601-duration": "2.1.2",
|
|
37
38
|
"isomorphic-git": "1.25.3",
|
|
38
39
|
"linux-release-info": "3.0.0",
|
package/src/commands/addon.js
CHANGED
|
@@ -11,6 +11,7 @@ import { getAllEnvVars } from '@clevercloud/client/esm/api/v2/addon.js';
|
|
|
11
11
|
import { sendToApi } from '../models/send-to-api.js';
|
|
12
12
|
import { toNameEqualsValueString } from '@clevercloud/client/esm/utils/env-vars.js';
|
|
13
13
|
import { resolveAddonId } from '../models/ids-resolver.js';
|
|
14
|
+
import { conf } from '../models/configuration.js';
|
|
14
15
|
|
|
15
16
|
const formatTable = initFormatTable();
|
|
16
17
|
|
|
@@ -114,13 +115,17 @@ function displayAddon (format, addon, providerName, message) {
|
|
|
114
115
|
name: 'Metabase',
|
|
115
116
|
urlEnv: 'METABASE_URL',
|
|
116
117
|
},
|
|
118
|
+
otoroshi: {
|
|
119
|
+
name: 'Otoroshi with LLM',
|
|
120
|
+
urlEnv: 'CC_OTOROSHI_URL',
|
|
121
|
+
},
|
|
117
122
|
};
|
|
118
123
|
|
|
119
124
|
const WIP_PROVIDERS = {
|
|
120
125
|
keycloak: {
|
|
121
126
|
status: 'beta',
|
|
122
127
|
postCreateInstructions: [
|
|
123
|
-
|
|
128
|
+
`Learn more about Keycloak on Clever Cloud: ${conf.DOC_URL}/addons/keycloak/`,
|
|
124
129
|
].join('\n'),
|
|
125
130
|
},
|
|
126
131
|
kv: {
|
|
@@ -129,25 +134,31 @@ function displayAddon (format, addon, providerName, message) {
|
|
|
129
134
|
colors.yellow('You can easily use Materia KV with \'redis-cli\', with such commands:'),
|
|
130
135
|
colors.blue(`source <(clever addon env ${addon.id} -F shell)`),
|
|
131
136
|
colors.blue('redis-cli -h $KV_HOST -p $KV_PORT --tls'),
|
|
132
|
-
|
|
137
|
+
`Learn more about Materia KV on Clever Cloud: ${conf.DOC_URL}/addons/materia-kv/`,
|
|
133
138
|
].join('\n'),
|
|
134
139
|
},
|
|
135
140
|
'addon-matomo': {
|
|
136
141
|
status: 'beta',
|
|
137
142
|
postCreateInstructions: [
|
|
138
|
-
|
|
143
|
+
`Learn more about Matomo on Clever Cloud: ${conf.DOC_URL}/addons/matomo/`,
|
|
139
144
|
].join('\n'),
|
|
140
145
|
},
|
|
141
146
|
metabase: {
|
|
142
147
|
status: 'beta',
|
|
143
148
|
postCreateInstructions: [
|
|
144
|
-
|
|
149
|
+
`Learn more about Metabase on Clever Cloud: ${conf.DOC_URL}/addons/metabase/`,
|
|
150
|
+
].join('\n'),
|
|
151
|
+
},
|
|
152
|
+
otoroshi: {
|
|
153
|
+
status: 'beta',
|
|
154
|
+
postCreateInstructions: [
|
|
155
|
+
`Learn more about Otoroshi with LLM on Clever Cloud: ${conf.DOC_URL}/addons/otoroshi/`,
|
|
145
156
|
].join('\n'),
|
|
146
157
|
},
|
|
147
158
|
'addon-pulsar': {
|
|
148
159
|
status: 'beta',
|
|
149
160
|
postCreateInstructions: [
|
|
150
|
-
|
|
161
|
+
`Learn more about Pulsar on Clever Cloud: ${conf.DOC_URL}/addons/pulsar/`,
|
|
151
162
|
].join('\n'),
|
|
152
163
|
},
|
|
153
164
|
};
|
|
@@ -200,7 +211,7 @@ function displayAddon (format, addon, providerName, message) {
|
|
|
200
211
|
Logger.println();
|
|
201
212
|
Logger.println(`Your ${provider.name} is starting:`);
|
|
202
213
|
Logger.println(` - Access it: ${urlToShow.startsWith('http') ? urlToShow : `https://${urlToShow}`}`);
|
|
203
|
-
Logger.println(` - Manage it:
|
|
214
|
+
Logger.println(` - Manage it: ${conf.GOTO_URL}/${addon.id}`);
|
|
204
215
|
}
|
|
205
216
|
|
|
206
217
|
if (providerName === 'keycloak') {
|
|
@@ -209,6 +220,13 @@ function displayAddon (format, addon, providerName, message) {
|
|
|
209
220
|
Logger.println(` - Admin user name: ${addon.env.find((e) => e.name === 'CC_KEYCLOAK_ADMIN').value}`);
|
|
210
221
|
Logger.println(` - Temporary password: ${addon.env.find((e) => e.name === 'CC_KEYCLOAK_ADMIN_DEFAULT_PASSWORD').value}`);
|
|
211
222
|
}
|
|
223
|
+
|
|
224
|
+
if (providerName === 'otoroshi') {
|
|
225
|
+
Logger.println();
|
|
226
|
+
Logger.println('An initial account has been created, change the password at first login (Security -> Administrators -> Edit user):');
|
|
227
|
+
Logger.println(` - Admin user name: ${addon.env.find((e) => e.name === 'CC_OTOROSHI_INITIAL_ADMIN_LOGIN').value}`);
|
|
228
|
+
Logger.println(` - Initial password: ${addon.env.find((e) => e.name === 'CC_OTOROSHI_INITIAL_ADMIN_PASSWORD').value}`);
|
|
229
|
+
}
|
|
212
230
|
}
|
|
213
231
|
|
|
214
232
|
if (providerName in WIP_PROVIDERS) {
|
package/src/commands/console.js
CHANGED
|
@@ -2,23 +2,22 @@ import * as Application from '../models/application.js';
|
|
|
2
2
|
import * as AppConfig from '../models/app_configuration.js';
|
|
3
3
|
import { Logger } from '../logger.js';
|
|
4
4
|
import openPage from 'open';
|
|
5
|
+
import { conf } from '../models/configuration.js';
|
|
5
6
|
|
|
6
7
|
export async function openConsole (params) {
|
|
7
8
|
const { alias, app: appIdOrName } = params.options;
|
|
8
9
|
|
|
9
|
-
const baseUrl = 'https://console.clever-cloud.com';
|
|
10
|
-
|
|
11
10
|
const { apps } = await AppConfig.loadApplicationConf();
|
|
12
11
|
// If no app is linked or asked, open the Console without any context
|
|
13
12
|
if (apps.length === 0 && !appIdOrName) {
|
|
14
13
|
Logger.println('Opening the Console in your browser');
|
|
15
|
-
await openPage(
|
|
14
|
+
await openPage(conf.CONSOLE_URL, { wait: false });
|
|
16
15
|
return;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
|
|
20
19
|
const prefixPath = (ownerId.startsWith('user_')) ? 'users/me' : `organisations/${ownerId}`;
|
|
21
|
-
const url = `${
|
|
20
|
+
const url = `${conf.CONSOLE_URL}/${prefixPath}/applications/${appId}`;
|
|
22
21
|
|
|
23
22
|
Logger.debug(`URL: ${url}`);
|
|
24
23
|
Logger.println(`Opening the Console in your browser for application ${appId}`);
|
package/src/commands/curl.js
CHANGED
|
@@ -16,11 +16,11 @@ async function loadTokens () {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function printCleverCurlHelp () {
|
|
19
|
-
const apiDocUrlv2 =
|
|
20
|
-
const apiDocUrlv4 =
|
|
19
|
+
const apiDocUrlv2 = `${conf.API_DOC_URL}/v2/`;
|
|
20
|
+
const apiDocUrlv4 = `${conf.API_DOC_URL}/v4/`;
|
|
21
21
|
|
|
22
22
|
Logger.println(`Usage: clever curl
|
|
23
|
-
Query Clever Cloud's API using Clever Tools credentials. For example:
|
|
23
|
+
Query Clever Cloud's API using Clever Tools credentials. For example:
|
|
24
24
|
|
|
25
25
|
clever curl ${conf.API_HOST}/v2/self
|
|
26
26
|
clever curl ${conf.API_HOST}/v2/summary
|
|
@@ -28,7 +28,7 @@ Query Clever Cloud's API using Clever Tools credentials. For example:
|
|
|
28
28
|
clever curl ${conf.API_HOST}/v2/organisations/<ORGANISATION_ID>/applications | jq '.[].id'
|
|
29
29
|
clever curl ${conf.API_HOST}/v4/billing/organisations/<ORGANISATION_ID>/<INVOICE_NUMBER>.pdf > invoice.pdf
|
|
30
30
|
|
|
31
|
-
Our API documentation is available here :
|
|
31
|
+
Our API documentation is available here :
|
|
32
32
|
|
|
33
33
|
${apiDocUrlv2}
|
|
34
34
|
${apiDocUrlv4}`);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { getFeatures, setFeature } from '../models/configuration.js';
|
|
2
|
+
import { EXPERIMENTAL_FEATURES } from '../experimental-features.js';
|
|
3
|
+
import { formatTable as initFormatTable } from '../format-table.js';
|
|
4
|
+
import { Logger } from '../logger.js';
|
|
5
|
+
|
|
6
|
+
const formatTable = initFormatTable();
|
|
7
|
+
|
|
8
|
+
export async function list (params) {
|
|
9
|
+
const { format } = params.options;
|
|
10
|
+
|
|
11
|
+
const featuresConf = await getFeatures();
|
|
12
|
+
// Add status from configuration file and remove instructions
|
|
13
|
+
const features = Object.entries(EXPERIMENTAL_FEATURES).map(([id, feature]) => {
|
|
14
|
+
const enabled = featuresConf[id] === true;
|
|
15
|
+
return {
|
|
16
|
+
id,
|
|
17
|
+
status: feature.status,
|
|
18
|
+
description: feature.description,
|
|
19
|
+
enabled,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// For each feature, print the object with the id, status, description and enabled
|
|
24
|
+
switch (format) {
|
|
25
|
+
case 'json': {
|
|
26
|
+
Logger.printJson(features);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case 'human':
|
|
30
|
+
default: {
|
|
31
|
+
const headers = ['ID', 'STATUS', 'DESCRIPTION', 'ENABLED'];
|
|
32
|
+
|
|
33
|
+
Logger.println(formatTable([
|
|
34
|
+
headers,
|
|
35
|
+
...features.map((feature) => [
|
|
36
|
+
feature.id,
|
|
37
|
+
feature.status,
|
|
38
|
+
feature.description,
|
|
39
|
+
feature.enabled,
|
|
40
|
+
]),
|
|
41
|
+
]));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function info (params) {
|
|
47
|
+
const { feature } = params.namedArgs;
|
|
48
|
+
const availableFeatures = Object.keys(EXPERIMENTAL_FEATURES);
|
|
49
|
+
|
|
50
|
+
if (!availableFeatures.includes(feature)) {
|
|
51
|
+
throw new Error(`Unavailable feature: ${feature}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Logger.println(EXPERIMENTAL_FEATURES[feature].instructions);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function enable (params) {
|
|
58
|
+
const { features } = params.namedArgs;
|
|
59
|
+
const availableFeatures = Object.keys(EXPERIMENTAL_FEATURES);
|
|
60
|
+
|
|
61
|
+
const unknownFeatures = features.filter((feature) => !availableFeatures.includes(feature));
|
|
62
|
+
if (unknownFeatures.length > 0) {
|
|
63
|
+
throw new Error(`Unavailable feature(s): ${unknownFeatures.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const featureName of features) {
|
|
67
|
+
await setFeature(featureName, true);
|
|
68
|
+
Logger.println(`Experimental feature '${featureName}' enabled`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (features.length === 1) {
|
|
72
|
+
Logger.println(EXPERIMENTAL_FEATURES[features[0]].instructions);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
Logger.println();
|
|
76
|
+
Logger.println("To learn more about these experimental features, use 'clever features info FEATURE_NAME'");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function disable (params) {
|
|
81
|
+
const { features } = params.namedArgs;
|
|
82
|
+
const availableFeatures = Object.keys(EXPERIMENTAL_FEATURES);
|
|
83
|
+
|
|
84
|
+
const unknownFeatures = features.filter((feature) => !availableFeatures.includes(feature));
|
|
85
|
+
if (unknownFeatures.length > 0) {
|
|
86
|
+
throw new Error(`Unavailable feature(s): ${unknownFeatures.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const featureName of features) {
|
|
90
|
+
await setFeature(featureName, false);
|
|
91
|
+
Logger.println(`Experimental feature '${featureName}' disabled`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
import colors from 'colors/safe.js';
|
|
3
|
+
import { Logger } from '../logger.js';
|
|
4
|
+
import { sendToApi } from '../models/send-to-api.js';
|
|
5
|
+
import { getAllEnvVars } from '@clevercloud/client/cjs/api/v2/addon.js';
|
|
6
|
+
import { findAddonsByNameOrId } from '../models/ids-resolver.js';
|
|
7
|
+
|
|
8
|
+
const URL_ENV_KEY = 'REDIS_URL';
|
|
9
|
+
const MAX_RETRIES_PER_REQUEST = 1;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Send a raw command to a compatible KV database
|
|
13
|
+
* @param {Object} params
|
|
14
|
+
* @param {Array<string>} params.args
|
|
15
|
+
* @param {Object} params.options
|
|
16
|
+
* @param {string} params.options.format
|
|
17
|
+
* @returns {Promise<void>}
|
|
18
|
+
*/
|
|
19
|
+
export async function sendRawCommand (params) {
|
|
20
|
+
const [addonIdOrRealIdOrName] = params.args;
|
|
21
|
+
const { org, format } = params.options;
|
|
22
|
+
|
|
23
|
+
const addons = await findAddonsByNameOrId(addonIdOrRealIdOrName, org);
|
|
24
|
+
|
|
25
|
+
if (addons.length === 0) {
|
|
26
|
+
throw new Error(`Add-on ${addonIdOrRealIdOrName} not found`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (addons.length > 1) {
|
|
30
|
+
const formattedAddons = addons
|
|
31
|
+
.map(({ addonId, ownerId }) => `\n${colors.grey(`- ${addonId} (${ownerId})`)}`)
|
|
32
|
+
.join('');
|
|
33
|
+
throw new Error(`Several add-ons found for '${addonIdOrRealIdOrName}', use ID instead:${formattedAddons}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { addonId, ownerId } = addons[0];
|
|
37
|
+
|
|
38
|
+
const url = await getAddonUrl(ownerId, addonId);
|
|
39
|
+
|
|
40
|
+
Logger.debug(`Extracted command: ${params.args.join(' ')}`);
|
|
41
|
+
const command = params.args.slice(1);
|
|
42
|
+
|
|
43
|
+
const result = await sendCommand(url, command);
|
|
44
|
+
|
|
45
|
+
switch (format) {
|
|
46
|
+
case 'json': {
|
|
47
|
+
Logger.printJson(result);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case 'human':
|
|
51
|
+
default: {
|
|
52
|
+
Logger.println(result);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the URL of the compatible KV database
|
|
59
|
+
* @param {string} ownerId
|
|
60
|
+
* @param {string} addonId
|
|
61
|
+
* @returns {Promise<string>} the URL of the compatible KV database
|
|
62
|
+
*/
|
|
63
|
+
async function getAddonUrl (ownerId, addonId) {
|
|
64
|
+
|
|
65
|
+
const envVars = await getAllEnvVars({ id: ownerId, addonId }).then(sendToApi);
|
|
66
|
+
const redisUrl = envVars.find((env) => env.name === URL_ENV_KEY)?.value;
|
|
67
|
+
|
|
68
|
+
if (!redisUrl) {
|
|
69
|
+
throw new Error(`Environment variable ${colors.red(URL_ENV_KEY)} not found, is it a Materia KV or Redis® add-on?`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return redisUrl;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Send a command to a compatible KV database
|
|
77
|
+
* @param {string} url
|
|
78
|
+
* @param {Array<string>} command
|
|
79
|
+
* @returns {Promise<string>} the command result
|
|
80
|
+
*/
|
|
81
|
+
async function sendCommand (url, command) {
|
|
82
|
+
Logger.debug(`Sending command '${command.join(' ')}' to ${url}`);
|
|
83
|
+
const client = new Redis(url, { maxRetriesPerRequest: MAX_RETRIES_PER_REQUEST });
|
|
84
|
+
try {
|
|
85
|
+
const result = await client.call(...command);
|
|
86
|
+
Logger.debug(`Command result: ${result}`);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await client.disconnect();
|
|
91
|
+
Logger.debug('Disconnected from server');
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/commands/profile.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import colors from 'colors/safe.js';
|
|
2
|
-
|
|
2
|
+
import openPage from 'open';
|
|
3
3
|
import { Logger } from '../logger.js';
|
|
4
4
|
import * as User from '../models/user.js';
|
|
5
5
|
|
|
@@ -46,3 +46,9 @@ export async function profile (params) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
|
+
|
|
50
|
+
export async function openProfile () {
|
|
51
|
+
const URL = 'https://console.clever-cloud.com/users/me/information';
|
|
52
|
+
Logger.debug('Opening the profile page in your browser');
|
|
53
|
+
await openPage(URL, { wait: false });
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const EXPERIMENTAL_FEATURES = {
|
|
2
|
+
kv: {
|
|
3
|
+
status: 'alpha',
|
|
4
|
+
description: 'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
|
|
5
|
+
instructions: `
|
|
6
|
+
Target any compatible add-on by its name or ID (with an org ID if needed) and send commands to it:
|
|
7
|
+
|
|
8
|
+
clever kv myMateriaKV SET myKey myValue
|
|
9
|
+
clever kv kv_xxxxxxxx GET myKey -F json
|
|
10
|
+
clever kv addon_xxxxx SET myTempKey myTempValue EX 120
|
|
11
|
+
clever kv myMateriaKV -o myOrg TTL myTempKey
|
|
12
|
+
clever kv redis_xxxxx --org org_xxxxx PING
|
|
13
|
+
|
|
14
|
+
Learn more about Materia KV: https://www.clever-cloud.com/developers/doc/addons/materia-kv/
|
|
15
|
+
`,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -11,13 +11,22 @@ const env = commonEnv(Logger);
|
|
|
11
11
|
const CONFIG_FILES = {
|
|
12
12
|
MAIN: 'clever-tools.json',
|
|
13
13
|
IDS_CACHE: 'ids-cache.json',
|
|
14
|
+
EXPERIMENTAL_FEATURES_FILE: 'clever-tools-experimental-features.json',
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
function
|
|
17
|
-
|
|
17
|
+
function getConfigDir () {
|
|
18
|
+
return (process.platform === 'win32')
|
|
18
19
|
? path.resolve(process.env.APPDATA, 'clever-cloud')
|
|
19
20
|
: xdg.basedir.configPath('clever-cloud');
|
|
20
|
-
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getConfigPath (configFile) {
|
|
24
|
+
return path.resolve(getConfigDir(), configFile);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Every function which need 'clever-cloud' directory, need to call it before
|
|
28
|
+
async function ensureConfigDirExists () {
|
|
29
|
+
await mkdirp(getConfigDir(), { mode: 0o700 });
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
export async function loadOAuthConf () {
|
|
@@ -49,9 +58,8 @@ export async function loadOAuthConf () {
|
|
|
49
58
|
|
|
50
59
|
export async function writeOAuthConf (oauthData) {
|
|
51
60
|
Logger.debug('Write the tokens in the configuration file…');
|
|
52
|
-
const configDir = path.dirname(conf.CONFIGURATION_FILE);
|
|
53
61
|
try {
|
|
54
|
-
await
|
|
62
|
+
await ensureConfigDirExists();
|
|
55
63
|
await fs.writeFile(conf.CONFIGURATION_FILE, JSON.stringify(oauthData));
|
|
56
64
|
}
|
|
57
65
|
catch (error) {
|
|
@@ -78,6 +86,7 @@ export async function writeIdsCache (ids) {
|
|
|
78
86
|
const cachePath = getConfigPath(CONFIG_FILES.IDS_CACHE);
|
|
79
87
|
const idsJson = JSON.stringify(ids);
|
|
80
88
|
try {
|
|
89
|
+
await ensureConfigDirExists();
|
|
81
90
|
await fs.writeFile(cachePath, idsJson);
|
|
82
91
|
}
|
|
83
92
|
catch (error) {
|
|
@@ -85,22 +94,48 @@ export async function writeIdsCache (ids) {
|
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
export async function getFeatures () {
|
|
98
|
+
Logger.debug('Get features configuration from ' + conf.EXPERIMENTAL_FEATURES_FILE);
|
|
99
|
+
try {
|
|
100
|
+
const rawFile = await fs.readFile(conf.EXPERIMENTAL_FEATURES_FILE);
|
|
101
|
+
return JSON.parse(rawFile);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error.code !== 'ENOENT') {
|
|
105
|
+
throw new Error(`Cannot get experimental features configuration from ${conf.EXPERIMENTAL_FEATURES_FILE}`);
|
|
106
|
+
}
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function setFeature (feature, value) {
|
|
112
|
+
const currentFeatures = await getFeatures();
|
|
113
|
+
const newFeatures = { ...currentFeatures, ...{ [feature]: value } };
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await ensureConfigDirExists();
|
|
117
|
+
await fs.writeFile(conf.EXPERIMENTAL_FEATURES_FILE, JSON.stringify(newFeatures, null, 2));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
throw new Error(`Cannot write experimental features configuration to ${conf.EXPERIMENTAL_FEATURES_FILE}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
88
124
|
export const conf = env.getOrElseAll({
|
|
89
125
|
API_HOST: 'https://api.clever-cloud.com',
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
LOG_HTTP_URL: 'https://api.clever-cloud.com/v2/logs/<%- appId %>',
|
|
93
|
-
EVENT_URL: 'wss://api.clever-cloud.com/v2/events/event-socket',
|
|
94
|
-
WARP_10_EXEC_URL: 'https://c1-warp10-clevercloud-customers.services.clever-cloud.com/api/v0/exec',
|
|
126
|
+
SSH_GATEWAY: 'ssh@sshgateway-clevercloud-customers.services.clever-cloud.com',
|
|
127
|
+
|
|
95
128
|
// the disclosure of these tokens is not considered as a vulnerability. Do not report this to our security service.
|
|
96
129
|
OAUTH_CONSUMER_KEY: 'T5nFjKeHH4AIlEveuGhB5S3xg8T19e',
|
|
97
130
|
OAUTH_CONSUMER_SECRET: 'MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT',
|
|
98
|
-
SSH_GATEWAY: 'ssh@sshgateway-clevercloud-customers.services.clever-cloud.com',
|
|
99
131
|
|
|
132
|
+
APP_CONFIGURATION_FILE: path.resolve('.', '.clever.json'),
|
|
100
133
|
CONFIGURATION_FILE: getConfigPath(CONFIG_FILES.MAIN),
|
|
101
|
-
|
|
102
|
-
// CONSOLE_TOKEN_URL: 'https://next-console.cleverapps.io/cli-oauth',
|
|
134
|
+
EXPERIMENTAL_FEATURES_FILE: getConfigPath(CONFIG_FILES.EXPERIMENTAL_FEATURES_FILE),
|
|
103
135
|
|
|
104
|
-
|
|
105
|
-
|
|
136
|
+
API_DOC_URL: 'https://www.clever-cloud.com/developers/api',
|
|
137
|
+
DOC_URL: 'https://www.clever-cloud.com/developers/doc',
|
|
138
|
+
CONSOLE_URL: 'https://console.clever-cloud.com',
|
|
139
|
+
CONSOLE_TOKEN_URL: 'https://console.clever-cloud.com/cli-oauth',
|
|
140
|
+
GOTO_URL: 'https://console.clever-cloud.com/goto',
|
|
106
141
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Logger } from '../logger.js';
|
|
2
2
|
import { sendToApi } from './send-to-api.js';
|
|
3
|
+
import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
|
|
3
4
|
import { loadIdsCache, writeIdsCache } from './configuration.js';
|
|
4
5
|
|
|
5
6
|
/*
|
|
@@ -102,3 +103,39 @@ async function getIdsFromSummary () {
|
|
|
102
103
|
|
|
103
104
|
return ids;
|
|
104
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the IDs and owners of found add-ons from a name, ID or real ID
|
|
109
|
+
* @param {string} addonIdOrRealIdOrName
|
|
110
|
+
* @param {{ orga_name?: string, orga_id?: string }} ownerNameOrId
|
|
111
|
+
* @throws {Error} if no add-on is found
|
|
112
|
+
* @throws {Error} if several add-ons are found
|
|
113
|
+
* @returns {Object} The ID and owner ID of the add-on { addonId, ownerId }
|
|
114
|
+
*/
|
|
115
|
+
export async function findAddonsByNameOrId (addonIdOrRealIdOrName, ownerNameOrId) {
|
|
116
|
+
const summary = await getSummary().then(sendToApi);
|
|
117
|
+
|
|
118
|
+
Logger.debug(`Searching for add-on '${addonIdOrRealIdOrName}' in ${summary.user.id} and ${summary.organisations.map((org) => org.id).join(', ')}`);
|
|
119
|
+
const candidates = [summary.user, ...summary.organisations]
|
|
120
|
+
.flatMap((owner) => owner.addons.map((addon) => ({ addon, owner })))
|
|
121
|
+
.filter(({ addon, owner }) => {
|
|
122
|
+
const matchOwner = ownerNameOrId == null
|
|
123
|
+
|| owner.id === ownerNameOrId.orga_id
|
|
124
|
+
|| owner.name === ownerNameOrId.orga_name;
|
|
125
|
+
const matchAddon = addon.name === addonIdOrRealIdOrName
|
|
126
|
+
|| addon.realId === addonIdOrRealIdOrName
|
|
127
|
+
|| addon.id === addonIdOrRealIdOrName;
|
|
128
|
+
return matchOwner && matchAddon;
|
|
129
|
+
})
|
|
130
|
+
.map(({ addon, owner }) => ({
|
|
131
|
+
addonId: addon.id,
|
|
132
|
+
ownerId: owner.id,
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
Logger.debug(`Found ${candidates.length} candidate(s):`);
|
|
136
|
+
for (const candidate of candidates) {
|
|
137
|
+
Logger.debug(` - ${candidate.addonId} (${candidate.ownerId})`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return candidates;
|
|
141
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Logger } from '../logger.js';
|
|
2
2
|
import { addOauthHeader } from '@clevercloud/client/esm/oauth.js';
|
|
3
|
-
import { conf, loadOAuthConf } from '
|
|
4
|
-
import { execWarpscript } from '@clevercloud/client/esm/request-warp10.superagent.js';
|
|
3
|
+
import { conf, loadOAuthConf } from './configuration.js';
|
|
5
4
|
import { prefixUrl } from '@clevercloud/client/esm/prefix-url.js';
|
|
6
5
|
import { request } from '@clevercloud/client/esm/request.fetch.js';
|
|
7
6
|
import { subtle as cryptoSuble } from 'node:crypto';
|
|
@@ -47,12 +46,6 @@ export function processError (error) {
|
|
|
47
46
|
throw error;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
export function sendToWarp10 (requestParams) {
|
|
51
|
-
return Promise.resolve(requestParams)
|
|
52
|
-
.then(prefixUrl(conf.WARP_10_EXEC_URL))
|
|
53
|
-
.then((requestParams) => execWarpscript(requestParams, { retry: 1 }));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
49
|
export async function getHostAndTokens () {
|
|
57
50
|
const tokens = await loadTokens();
|
|
58
51
|
return {
|