clever-tools 3.4.0 → 3.5.1
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 +0 -1
- package/bin/clever.js +28 -6
- package/package.json +4 -2
- package/src/commands/addon.js +68 -16
- package/src/commands/create.js +16 -4
- package/src/commands/curl.js +72 -22
- package/src/commands/database.js +46 -24
- package/src/commands/drain.js +10 -4
- package/src/commands/logs.js +6 -5
- package/src/commands/status.js +2 -1
- package/src/models/application.js +4 -2
- package/src/models/drain.js +27 -5
- package/src/models/log-v4.js +3 -2
- package/src/parsers.js +53 -3
package/README.md
CHANGED
|
@@ -92,7 +92,6 @@ Where region is one of:
|
|
|
92
92
|
- `rbx` (Roubaix, OVHcloud)
|
|
93
93
|
- `rbxhds` (Roubaix, HDS servers, OVHcloud)
|
|
94
94
|
- `scw` (Paris, [Scaleway DC5](https://www.clever-cloud.com/blog/press/2023/01/17/clever-cloud-and-scaleway-join-forces-to-unveil-a-sovereign-european-paas-offering/))
|
|
95
|
-
- `jed` (Jeddah, Oracle Cloud)
|
|
96
95
|
- `mtl` (Montreal, OVHcloud)
|
|
97
96
|
- `sgp` (Singapore, OVHcloud)
|
|
98
97
|
- `syd` (Sydney, OVHcloud)
|
package/bin/clever.js
CHANGED
|
@@ -169,7 +169,7 @@ function run () {
|
|
|
169
169
|
metavar: 'before',
|
|
170
170
|
aliases: ['until'],
|
|
171
171
|
parser: Parsers.date,
|
|
172
|
-
description: 'Fetch logs before this date/time (ISO8601)',
|
|
172
|
+
description: 'Fetch logs before this date/time (ISO8601 date or duration, positive number in seconds or duration Ex: 1h)',
|
|
173
173
|
}),
|
|
174
174
|
branch: cliparse.option('branch', {
|
|
175
175
|
aliases: ['b'],
|
|
@@ -310,7 +310,7 @@ function run () {
|
|
|
310
310
|
}),
|
|
311
311
|
addonPlan: cliparse.option('plan', {
|
|
312
312
|
aliases: ['p'],
|
|
313
|
-
default: '
|
|
313
|
+
default: '',
|
|
314
314
|
metavar: 'plan',
|
|
315
315
|
description: 'Addon plan, depends on the provider',
|
|
316
316
|
complete: Addon('completePlan'),
|
|
@@ -360,6 +360,12 @@ function run () {
|
|
|
360
360
|
metavar: 'token',
|
|
361
361
|
description: 'Directly give an existing token',
|
|
362
362
|
}),
|
|
363
|
+
taskCommand: cliparse.option('task', {
|
|
364
|
+
description: 'The application launch as a task executing the given command, then stopped',
|
|
365
|
+
aliases: ['T'],
|
|
366
|
+
parser: Parsers.nonEmptyString,
|
|
367
|
+
metavar: 'command',
|
|
368
|
+
}),
|
|
363
369
|
instanceType: cliparse.option('type', {
|
|
364
370
|
aliases: ['t'],
|
|
365
371
|
required: true,
|
|
@@ -377,6 +383,12 @@ function run () {
|
|
|
377
383
|
metavar: 'api_key',
|
|
378
384
|
description: 'Drain custom key',
|
|
379
385
|
}),
|
|
386
|
+
drainIndexPrefix: cliparse.option('index-prefix', {
|
|
387
|
+
aliases: ['i'],
|
|
388
|
+
metavar: 'index_prefix',
|
|
389
|
+
description: 'Optional drain index prefix for ElasticSearch: `<indexPrefix>-<YYYY-MM-DD>`',
|
|
390
|
+
default: 'logstash-<YYYY-MM-DD>',
|
|
391
|
+
}),
|
|
380
392
|
verbose: cliparse.flag('verbose', { aliases: ['v'], description: 'Verbose output' }),
|
|
381
393
|
withoutCache: cliparse.flag('without-cache', { description: 'Restart the application without using cache' }),
|
|
382
394
|
confirmAddonCreation: cliparse.flag('yes', {
|
|
@@ -596,7 +608,7 @@ function run () {
|
|
|
596
608
|
|
|
597
609
|
const addonCommands = cliparse.command('addon', {
|
|
598
610
|
description: 'Manage addons',
|
|
599
|
-
options: [opts.orgaIdOrName],
|
|
611
|
+
options: [opts.orgaIdOrName, opts.humanJsonOutputFormat],
|
|
600
612
|
commands: [addonCreateCommand, addonDeleteCommand, addonRenameCommand, addonProvidersCommand, addonEnvCommand],
|
|
601
613
|
}, addon('list'));
|
|
602
614
|
|
|
@@ -639,9 +651,15 @@ function run () {
|
|
|
639
651
|
const appCreateCommand = cliparse.command('create', {
|
|
640
652
|
description: 'Create an application',
|
|
641
653
|
args: [args.appNameCreation],
|
|
642
|
-
options: [opts.instanceType, opts.orgaIdOrName, opts.aliasCreation, opts.region, opts.github, opts.humanJsonOutputFormat],
|
|
654
|
+
options: [opts.instanceType, opts.orgaIdOrName, opts.aliasCreation, opts.region, opts.github, opts.humanJsonOutputFormat, opts.taskCommand],
|
|
643
655
|
}, create('create'));
|
|
644
656
|
|
|
657
|
+
// CURL COMMAND
|
|
658
|
+
// NOTE: it's just here for documentation purposes, look at the bottom of the file for the real "clever curl" command
|
|
659
|
+
const curlCommand = cliparse.command('curl', {
|
|
660
|
+
description: 'Query Clever Cloud\'s API using Clever Tools credentials',
|
|
661
|
+
}, () => null);
|
|
662
|
+
|
|
645
663
|
// DELETE COMMAND
|
|
646
664
|
const deleteCommandModule = lazyRequirePromiseModule('../src/commands/delete.js');
|
|
647
665
|
const deleteCommand = cliparse.command('delete', {
|
|
@@ -695,7 +713,7 @@ function run () {
|
|
|
695
713
|
const drainCreateCommand = cliparse.command('create', {
|
|
696
714
|
description: 'Create a drain',
|
|
697
715
|
args: [args.drainType, args.drainUrl],
|
|
698
|
-
options: [opts.addonId, opts.drainUsername, opts.drainPassword, opts.drainAPIKey],
|
|
716
|
+
options: [opts.addonId, opts.drainUsername, opts.drainPassword, opts.drainAPIKey, opts.drainIndexPrefix],
|
|
699
717
|
}, drain('create'));
|
|
700
718
|
const drainRemoveCommand = cliparse.command('remove', {
|
|
701
719
|
description: 'Remove a drain',
|
|
@@ -1031,7 +1049,7 @@ function run () {
|
|
|
1031
1049
|
const backupsCommand = cliparse.command('backups', {
|
|
1032
1050
|
description: 'List available database backups',
|
|
1033
1051
|
args: [args.databaseId],
|
|
1034
|
-
options: [opts.orgaIdOrName],
|
|
1052
|
+
options: [opts.orgaIdOrName, opts.humanJsonOutputFormat],
|
|
1035
1053
|
commands: [
|
|
1036
1054
|
downloadBackupCommand,
|
|
1037
1055
|
],
|
|
@@ -1045,6 +1063,9 @@ function run () {
|
|
|
1045
1063
|
console.info('clever database backups download');
|
|
1046
1064
|
});
|
|
1047
1065
|
|
|
1066
|
+
// Patch help command description
|
|
1067
|
+
cliparseCommands.helpCommand.description = 'Display help about the Clever Cloud CLI';
|
|
1068
|
+
|
|
1048
1069
|
const commands = _sortBy([
|
|
1049
1070
|
accesslogsCommand,
|
|
1050
1071
|
activityCommand,
|
|
@@ -1055,6 +1076,7 @@ function run () {
|
|
|
1055
1076
|
appUnlinkCommand,
|
|
1056
1077
|
cancelDeployCommand,
|
|
1057
1078
|
configCommands,
|
|
1079
|
+
curlCommand,
|
|
1058
1080
|
databaseCommand,
|
|
1059
1081
|
deleteCommand,
|
|
1060
1082
|
deployCommand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clever-tools",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"description": "Command Line Interface for Clever Cloud.",
|
|
5
5
|
"main": "bin/clever.js",
|
|
6
6
|
"keywords": [
|
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
"cliparse": "^0.3.3",
|
|
31
31
|
"colors": "1.4.0",
|
|
32
32
|
"common-env": "^6.4.0",
|
|
33
|
-
"curlconverter": "^
|
|
33
|
+
"curlconverter": "^4.9.0",
|
|
34
|
+
"duration-js": "^4.0.0",
|
|
34
35
|
"eventsource": "^1.1.0",
|
|
36
|
+
"iso8601-duration": "^2.1.2",
|
|
35
37
|
"isomorphic-git": "^1.25.3",
|
|
36
38
|
"linux-release-info": "^3.0.0",
|
|
37
39
|
"lodash": "^4.17.21",
|
package/src/commands/addon.js
CHANGED
|
@@ -15,28 +15,66 @@ const { sendToApi } = require('../models/send-to-api.js');
|
|
|
15
15
|
const { toNameEqualsValueString } = require('@clevercloud/client/cjs/utils/env-vars.js');
|
|
16
16
|
const { resolveAddonId } = require('../models/ids-resolver.js');
|
|
17
17
|
|
|
18
|
+
function getPlan (inputPlan, providerName) {
|
|
19
|
+
|
|
20
|
+
// Return user plan
|
|
21
|
+
if (inputPlan !== '') {
|
|
22
|
+
return inputPlan;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Choose a default plan based on the add-on provider
|
|
26
|
+
switch (providerName) {
|
|
27
|
+
case 'kv':
|
|
28
|
+
return 'alpha';
|
|
29
|
+
default:
|
|
30
|
+
return 'dev';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
async function list (params) {
|
|
19
|
-
const { org: orgaIdOrName } = params.options;
|
|
35
|
+
const { org: orgaIdOrName, format } = params.options;
|
|
20
36
|
|
|
21
37
|
const ownerId = await Organisation.getId(orgaIdOrName);
|
|
22
38
|
const addons = await Addon.list(ownerId);
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
switch (format) {
|
|
41
|
+
case 'json': {
|
|
42
|
+
const formattedAddons = addons.map((addon) => {
|
|
43
|
+
return {
|
|
44
|
+
addonId: addon.id,
|
|
45
|
+
creationDate: addon.creationDate,
|
|
46
|
+
name: addon.name,
|
|
47
|
+
planName: addon.plan.name,
|
|
48
|
+
planSlug: addon.plan.slug,
|
|
49
|
+
providerId: addon.provider.id,
|
|
50
|
+
realId: addon.realId,
|
|
51
|
+
region: addon.region,
|
|
52
|
+
type: addon.provider.name,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
Logger.printJson(formattedAddons);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case 'human':
|
|
59
|
+
default: {
|
|
60
|
+
const formattedAddons = addons.map((addon) => {
|
|
61
|
+
return [
|
|
62
|
+
addon.plan.name + ' ' + addon.provider.name,
|
|
63
|
+
addon.region,
|
|
64
|
+
colors.bold.green(addon.name),
|
|
65
|
+
addon.id,
|
|
66
|
+
];
|
|
67
|
+
});
|
|
68
|
+
Logger.println(formatTable(formattedAddons));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
33
71
|
}
|
|
34
72
|
|
|
35
73
|
async function create (params) {
|
|
36
74
|
const [providerName, name] = params.args;
|
|
37
75
|
const {
|
|
38
76
|
link: linkedAppAlias,
|
|
39
|
-
plan:
|
|
77
|
+
plan: inputPlan,
|
|
40
78
|
region,
|
|
41
79
|
yes: skipConfirmation,
|
|
42
80
|
org: orgaIdOrName,
|
|
@@ -44,6 +82,7 @@ async function create (params) {
|
|
|
44
82
|
} = params.options;
|
|
45
83
|
const version = params.options['addon-version'];
|
|
46
84
|
const addonOptions = parseAddonOptions(params.options.option);
|
|
85
|
+
const planName = getPlan(inputPlan, providerName);
|
|
47
86
|
|
|
48
87
|
const ownerId = (orgaIdOrName != null)
|
|
49
88
|
? await Organisation.getId(orgaIdOrName)
|
|
@@ -70,23 +109,26 @@ async function create (params) {
|
|
|
70
109
|
ownerId: linkedAppData.ownerId,
|
|
71
110
|
});
|
|
72
111
|
await Addon.link(linkedAppData.ownerId, linkedAppData.appId, { addon_id: newAddon.id });
|
|
73
|
-
displayAddon(format, newAddon, `Add-on created and linked to application ${linkedAppAlias} successfully!`);
|
|
112
|
+
displayAddon(format, newAddon, providerName, `Add-on created and linked to application ${linkedAppAlias} successfully!`);
|
|
74
113
|
}
|
|
75
114
|
else {
|
|
76
115
|
const newAddon = await Addon.create(addonToCreate);
|
|
77
|
-
displayAddon(format, newAddon, 'Add-on created successfully!');
|
|
116
|
+
displayAddon(format, newAddon, providerName, 'Add-on created successfully!');
|
|
78
117
|
}
|
|
79
118
|
}
|
|
80
119
|
|
|
81
|
-
function displayAddon (format, addon, message) {
|
|
120
|
+
function displayAddon (format, addon, providerName, message) {
|
|
82
121
|
switch (format) {
|
|
83
122
|
|
|
84
123
|
case 'json': {
|
|
85
|
-
|
|
124
|
+
const jsonAddon = {
|
|
86
125
|
id: addon.id,
|
|
87
126
|
realId: addon.realId,
|
|
88
127
|
name: addon.name,
|
|
89
|
-
}
|
|
128
|
+
};
|
|
129
|
+
Logger.printJson((providerName === 'kv')
|
|
130
|
+
? { ...jsonAddon, availability: 'alpha', warning: 'Don\'t store sensitive or production grade data' }
|
|
131
|
+
: jsonAddon);
|
|
90
132
|
break;
|
|
91
133
|
}
|
|
92
134
|
|
|
@@ -98,6 +140,16 @@ function displayAddon (format, addon, message) {
|
|
|
98
140
|
`Real ID: ${addon.realId}`,
|
|
99
141
|
`Name: ${addon.name}`,
|
|
100
142
|
].join('\n'));
|
|
143
|
+
if (providerName === 'kv') {
|
|
144
|
+
const materiaMessage = [
|
|
145
|
+
'',
|
|
146
|
+
colors.yellow(`/!\\ The MateriaDB ${providerName.toUpperCase()} provider is in Alpha testing phase, don't store sensitive or production grade data`),
|
|
147
|
+
'You can easily use MateriaDB KV with \'redis-cli\', with such commands:',
|
|
148
|
+
colors.blue(`source <(clever addon env ${addon.id} -F shell)`),
|
|
149
|
+
colors.blue('redis-cli -h $KV_HOST -p $KV_PORT'),
|
|
150
|
+
].join('\n');
|
|
151
|
+
Logger.println(materiaMessage);
|
|
152
|
+
}
|
|
101
153
|
}
|
|
102
154
|
}
|
|
103
155
|
|
package/src/commands/create.js
CHANGED
|
@@ -8,24 +8,30 @@ const Logger = require('../logger.js');
|
|
|
8
8
|
async function create (params) {
|
|
9
9
|
const { type: typeName } = params.options;
|
|
10
10
|
const [rawName] = params.args;
|
|
11
|
-
const { org: orgaIdOrName, alias, region, github: githubOwnerRepo, format } = params.options;
|
|
11
|
+
const { org: orgaIdOrName, alias, region, github: githubOwnerRepo, format, task: taskCommand } = params.options;
|
|
12
12
|
const { apps } = await AppConfig.loadApplicationConf();
|
|
13
13
|
|
|
14
14
|
// Application name is optionnal, use current directory name if not specified (empty string)
|
|
15
15
|
const name = (rawName !== '') ? rawName : getCurrentDirectoryName();
|
|
16
16
|
|
|
17
|
+
const isTask = (taskCommand != null);
|
|
18
|
+
const envVars = isTask
|
|
19
|
+
? { CC_RUN_COMMAND: taskCommand }
|
|
20
|
+
: {};
|
|
21
|
+
|
|
17
22
|
AppConfig.checkAlreadyLinked(apps, name, alias);
|
|
18
23
|
|
|
19
24
|
const github = getGithubDetails(githubOwnerRepo);
|
|
20
|
-
const app = await Application.create(name, typeName, region, orgaIdOrName, github);
|
|
25
|
+
const app = await Application.create(name, typeName, region, orgaIdOrName, github, isTask, envVars);
|
|
21
26
|
await AppConfig.addLinkedApplication(app, alias);
|
|
22
27
|
|
|
23
28
|
switch (format) {
|
|
24
|
-
|
|
25
29
|
case 'json': {
|
|
26
30
|
Logger.printJson({
|
|
27
31
|
id: app.id,
|
|
28
32
|
name: app.name,
|
|
33
|
+
executedAs: app.instance.lifetime,
|
|
34
|
+
env: app.env,
|
|
29
35
|
deployUrl: app.deployUrl,
|
|
30
36
|
});
|
|
31
37
|
break;
|
|
@@ -33,7 +39,13 @@ async function create (params) {
|
|
|
33
39
|
|
|
34
40
|
case 'human':
|
|
35
41
|
default:
|
|
36
|
-
|
|
42
|
+
if (isTask) {
|
|
43
|
+
Logger.println('Your application has been successfully created as a task!');
|
|
44
|
+
Logger.println(`The "CC_RUN_COMMAND" environment variable has been set to "${taskCommand}"`);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
Logger.println('Your application has been successfully created!');
|
|
48
|
+
}
|
|
37
49
|
Logger.println(`ID: ${app.id}`);
|
|
38
50
|
Logger.println(`Name: ${name}`);
|
|
39
51
|
}
|
package/src/commands/curl.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { parseCurlCommand } = require('curlconverter/util.js');
|
|
4
3
|
const { spawn } = require('child_process');
|
|
5
4
|
const { loadOAuthConf, conf } = require('../models/configuration.js');
|
|
6
5
|
const { addOauthHeader } = require('@clevercloud/client/cjs/oauth.js');
|
|
6
|
+
const Logger = require('../logger.js');
|
|
7
|
+
const colors = require('colors/safe');
|
|
7
8
|
|
|
8
9
|
async function loadTokens () {
|
|
9
10
|
const tokens = await loadOAuthConf();
|
|
@@ -15,37 +16,86 @@ async function loadTokens () {
|
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function printCleverCurlHelp () {
|
|
20
|
+
const apiDocUrlv2 = 'https://developers.clever-cloud.com/api/v2/';
|
|
21
|
+
const apiDocUrlv4 = 'https://developers.clever-cloud.com/api/v4/';
|
|
22
|
+
|
|
23
|
+
Logger.println(`Usage: clever curl
|
|
24
|
+
Query Clever Cloud's API using Clever Tools credentials. For example:
|
|
25
|
+
|
|
26
|
+
clever curl ${conf.API_HOST}/v2/self
|
|
27
|
+
clever curl ${conf.API_HOST}/v2/summary
|
|
28
|
+
clever curl ${conf.API_HOST}/v4/products/zones
|
|
29
|
+
clever curl ${conf.API_HOST}/v2/organisations/<ORGANISATION_ID>/applications | jq '.[].id'
|
|
30
|
+
clever curl ${conf.API_HOST}/v4/billing/organisations/<ORGANISATION_ID>/<INVOICE_NUMBER>.pdf > invoice.pdf
|
|
31
|
+
|
|
32
|
+
Our API documentation is available here :
|
|
33
|
+
|
|
34
|
+
${apiDocUrlv2}
|
|
35
|
+
${apiDocUrlv4}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
async function curl () {
|
|
19
39
|
|
|
20
|
-
// We
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
// We remove the first three args: "node", "clever" and "curl"
|
|
41
|
+
const curlArgs = process.argv.slice(3);
|
|
42
|
+
const hasNoArgs = curlArgs.length === 0;
|
|
43
|
+
const startsWithHelpArg = curlArgs[0] === '--help' || curlArgs[0] === '-h';
|
|
44
|
+
const shouldDisplayCleverCurlHelp = hasNoArgs || startsWithHelpArg;
|
|
25
45
|
|
|
26
|
-
|
|
46
|
+
if (shouldDisplayCleverCurlHelp) {
|
|
47
|
+
printCleverCurlHelp();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
27
50
|
|
|
28
|
-
const
|
|
51
|
+
const requestParams = await parseCurlCommand(['curl', ...curlArgs]);
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
// We only allow request to the respective API_HOST
|
|
54
|
+
if (!requestParams.url.startsWith(conf.API_HOST)) {
|
|
55
|
+
Logger.error('"clever curl" command must be used with ' + colors.blue(conf.API_HOST));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const lastCurlArg = curlArgs.at(-1);
|
|
60
|
+
const lastCurlArgIsHelp = lastCurlArg !== '--help' && lastCurlArg !== '-h';
|
|
61
|
+
|
|
62
|
+
// Add oAuth header, only if last cURL arg is not help
|
|
63
|
+
// We do this because cURL's help arg expect a category
|
|
64
|
+
if (lastCurlArgIsHelp) {
|
|
36
65
|
|
|
37
|
-
|
|
38
|
-
.
|
|
39
|
-
|
|
66
|
+
const tokens = await loadTokens();
|
|
67
|
+
const oauthHeader = await Promise.resolve(requestParams)
|
|
68
|
+
.then(addOauthHeader(tokens))
|
|
69
|
+
.then((request) => request.headers.Authorization);
|
|
40
70
|
|
|
41
|
-
|
|
42
|
-
|
|
71
|
+
curlArgs.push('-H', `Authorization: ${oauthHeader}`);
|
|
72
|
+
}
|
|
43
73
|
|
|
44
|
-
|
|
45
|
-
curlParams.push('-H', `Authorization: ${oauthHeader}`);
|
|
74
|
+
spawn('curl', curlArgs, { stdio: 'inherit' });
|
|
46
75
|
|
|
47
|
-
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function parseCurlCommand (curlCommand) {
|
|
79
|
+
|
|
80
|
+
const curlParser = await import('curlconverter/dist/src/parse.js');
|
|
81
|
+
|
|
82
|
+
const [request] = curlParser.parse(curlCommand);
|
|
83
|
+
const url = request.urls[0];
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
method: url.method.toString(),
|
|
87
|
+
url: url.urlWithoutQueryArray.toString(),
|
|
88
|
+
headers: transformCurlConverterWordsToObject(request.headers.headers),
|
|
89
|
+
queryParams: transformCurlConverterWordsToObject(url.queryDict),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
48
92
|
|
|
93
|
+
function transformCurlConverterWordsToObject (words = []) {
|
|
94
|
+
return Object.fromEntries(
|
|
95
|
+
words.map(([key, value]) => {
|
|
96
|
+
return [key.toString(), value.toString()];
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
49
99
|
}
|
|
50
100
|
|
|
51
101
|
module.exports = { curl };
|
package/src/commands/database.js
CHANGED
|
@@ -7,41 +7,63 @@ const formatTable = require('../format-table')();
|
|
|
7
7
|
const superagent = require('superagent');
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const { findOwnerId } = require('../models/addon.js');
|
|
10
|
-
const { resolveRealId } = require('../models/ids-resolver.js');
|
|
10
|
+
const { resolveRealId, resolveAddonId } = require('../models/ids-resolver.js');
|
|
11
|
+
const Logger = require('../logger.js');
|
|
11
12
|
|
|
12
13
|
async function listBackups (params) {
|
|
13
14
|
|
|
14
|
-
const { org } = params.options;
|
|
15
|
+
const { org, format } = params.options;
|
|
15
16
|
const [addonIdOrRealId] = params.args;
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
18
|
+
const realId = await resolveRealId(addonIdOrRealId);
|
|
19
|
+
const addonId = await resolveAddonId(addonIdOrRealId);
|
|
20
|
+
const ownerId = await findOwnerId(org, realId);
|
|
19
21
|
|
|
20
|
-
const backups = await getBackups({ ownerId, ref:
|
|
22
|
+
const backups = await getBackups({ ownerId, ref: realId }).then(sendToApi);
|
|
21
23
|
|
|
22
|
-
if (backups.length === 0) {
|
|
24
|
+
if (backups.length === 0 && format === 'human') {
|
|
23
25
|
println('There are no backups yet');
|
|
24
26
|
return;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
backup
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
const sortedBackups = backups.sort((a, b) => a.creation_date.localeCompare(b.creation_date));
|
|
30
|
+
|
|
31
|
+
switch (format) {
|
|
32
|
+
case 'json': {
|
|
33
|
+
const formattedBackups = sortedBackups.map((backup) => {
|
|
34
|
+
return {
|
|
35
|
+
addonId: addonId,
|
|
36
|
+
backupId: backup.backup_id,
|
|
37
|
+
creationDate: backup.creation_date,
|
|
38
|
+
downloadUrl: backup.download_url,
|
|
39
|
+
ownerId: ownerId,
|
|
40
|
+
realId: realId,
|
|
41
|
+
status: backup.status,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
Logger.printJson(formattedBackups);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case 'human': {
|
|
48
|
+
const formattedLines = sortedBackups
|
|
49
|
+
.map((backup) => [
|
|
50
|
+
backup.backup_id,
|
|
51
|
+
backup.creation_date,
|
|
52
|
+
backup.status,
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const head = [
|
|
56
|
+
'BACKUP ID',
|
|
57
|
+
'CREATION DATE',
|
|
58
|
+
'STATUS',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
println(formatTable([
|
|
62
|
+
head,
|
|
63
|
+
...formattedLines,
|
|
64
|
+
]));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
45
67
|
}
|
|
46
68
|
|
|
47
69
|
async function downloadBackups (params) {
|
package/src/commands/drain.js
CHANGED
|
@@ -25,16 +25,22 @@ async function list (params) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
drains.forEach((drain) => {
|
|
28
|
-
const { id, state, target
|
|
29
|
-
|
|
28
|
+
const { id, state, target } = drain;
|
|
29
|
+
const { url, drainType, indexPrefix } = target;
|
|
30
|
+
|
|
31
|
+
let drainView = `${id} -> ${state} for ${url} as ${drainType}`;
|
|
32
|
+
if (indexPrefix != null) {
|
|
33
|
+
drainView += `, index: '${indexPrefix}-<YYYY-MM-DD>'`;
|
|
34
|
+
}
|
|
35
|
+
Logger.println(drainView);
|
|
30
36
|
});
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
async function create (params) {
|
|
34
40
|
const [drainTargetType, drainTargetURL] = params.args;
|
|
35
|
-
const { alias, addon: addonId, username, password, 'api-key': apiKey } = params.options;
|
|
41
|
+
const { alias, addon: addonId, username, password, 'api-key': apiKey, 'index-prefix': indexPrefix } = params.options;
|
|
36
42
|
const drainTargetCredentials = { username, password };
|
|
37
|
-
const drainTargetConfig = { apiKey };
|
|
43
|
+
const drainTargetConfig = { apiKey, indexPrefix };
|
|
38
44
|
|
|
39
45
|
const appIdOrAddonId = await getAppOrAddonId({ alias, addonId });
|
|
40
46
|
const body = createDrainBody(appIdOrAddonId, drainTargetURL, drainTargetType, drainTargetCredentials, drainTargetConfig);
|
package/src/commands/logs.js
CHANGED
|
@@ -6,19 +6,18 @@ const Log = require('../models/log-v4.js');
|
|
|
6
6
|
const Logger = require('../logger.js');
|
|
7
7
|
const { Deferred } = require('../models/utils.js');
|
|
8
8
|
const colors = require('colors/safe');
|
|
9
|
+
const { resolveAddonId } = require('../models/ids-resolver.js');
|
|
9
10
|
|
|
10
11
|
async function appLogs (params) {
|
|
11
|
-
const { alias, addon:
|
|
12
|
+
const { alias, addon: addonIdOrRealId, after: since, before: until, search, 'deployment-id': deploymentId, format } = params.options;
|
|
12
13
|
|
|
13
14
|
// ignore --search ""
|
|
14
15
|
const filter = (search !== '') ? search : null;
|
|
15
|
-
|
|
16
|
-
const { appId, ownerId } = await AppConfig.getAppDetails({ alias });
|
|
17
|
-
|
|
18
16
|
const isForHuman = (format === 'human');
|
|
19
17
|
|
|
20
18
|
// TODO: drop when addons are migrated to the v4 API
|
|
21
|
-
if (
|
|
19
|
+
if (addonIdOrRealId != null) {
|
|
20
|
+
const addonId = await resolveAddonId(addonIdOrRealId);
|
|
22
21
|
if (isForHuman) {
|
|
23
22
|
Logger.println(colors.blue('Waiting for addon logs…'));
|
|
24
23
|
}
|
|
@@ -28,6 +27,8 @@ async function appLogs (params) {
|
|
|
28
27
|
return LogV2.displayLogs({ appAddonId: addonId, since, until, filter, deploymentId });
|
|
29
28
|
}
|
|
30
29
|
|
|
30
|
+
const { appId, ownerId } = await AppConfig.getAppDetails({ alias });
|
|
31
|
+
|
|
31
32
|
if (isForHuman) {
|
|
32
33
|
Logger.println(colors.blue('Waiting for application logs…'));
|
|
33
34
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -35,11 +35,12 @@ function computeStatus (instances, app) {
|
|
|
35
35
|
: colors.bold.red('stopped');
|
|
36
36
|
|
|
37
37
|
const statusLine = `${app.name}: ${statusMessage}`;
|
|
38
|
+
const taskLine = `Executed as: ${colors.bold(app.instance.lifetime)}`;
|
|
38
39
|
const deploymentLine = isDeploying
|
|
39
40
|
? `Deployment in progress ${displayGroupInfo(deployingInstances, deployingCommit)}`
|
|
40
41
|
: '';
|
|
41
42
|
|
|
42
|
-
return [statusLine, deploymentLine].join('\n');
|
|
43
|
+
return [statusLine, taskLine, deploymentLine].join('\n');
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
function displayScalability (app) {
|
|
@@ -17,7 +17,7 @@ function listAvailableTypes () {
|
|
|
17
17
|
return autocomplete.words(['docker', 'elixir', 'go', 'gradle', 'haskell', 'jar', 'maven', 'meteor', 'node', 'php', 'play1', 'play2', 'python', 'ruby', 'rust', 'sbt', 'static-apache', 'war']);
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
const AVAILABLE_ZONES = ['par', 'grahds', 'rbx', 'rbxhds', 'scw', '
|
|
20
|
+
const AVAILABLE_ZONES = ['par', 'grahds', 'rbx', 'rbxhds', 'scw', 'mtl', 'sgp', 'syd', 'wsw'];
|
|
21
21
|
|
|
22
22
|
function listAvailableZones () {
|
|
23
23
|
return autocomplete.words(AVAILABLE_ZONES);
|
|
@@ -53,7 +53,7 @@ async function getInstanceType (type) {
|
|
|
53
53
|
return instanceVariant;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
async function create (name, typeName, region, orgaIdOrName, github) {
|
|
56
|
+
async function create (name, typeName, region, orgaIdOrName, github, isTask, envVars) {
|
|
57
57
|
Logger.debug('Create the application…');
|
|
58
58
|
|
|
59
59
|
const ownerId = (orgaIdOrName != null)
|
|
@@ -74,6 +74,8 @@ async function create (name, typeName, region, orgaIdOrName, github) {
|
|
|
74
74
|
minInstances: 1,
|
|
75
75
|
name: name,
|
|
76
76
|
zone: region,
|
|
77
|
+
instanceLifetime: isTask ? 'TASK' : 'REGULAR',
|
|
78
|
+
env: envVars,
|
|
77
79
|
};
|
|
78
80
|
|
|
79
81
|
if (github != null) {
|
package/src/models/drain.js
CHANGED
|
@@ -6,7 +6,7 @@ const DRAIN_TYPES = [
|
|
|
6
6
|
{ id: 'TCPSyslog' },
|
|
7
7
|
{ id: 'UDPSyslog' },
|
|
8
8
|
{ id: 'HTTP', credentials: 'OPTIONAL' },
|
|
9
|
-
{ id: 'ElasticSearch', credentials: 'MANDATORY' },
|
|
9
|
+
{ id: 'ElasticSearch', credentials: 'MANDATORY', indexPrefix: 'OPTIONAL' },
|
|
10
10
|
{ id: 'DatadogHTTP' },
|
|
11
11
|
{ id: 'NewRelicHTTP', apiKey: 'MANDATORY' },
|
|
12
12
|
];
|
|
@@ -30,14 +30,18 @@ function createDrainBody (appId, drainTargetURL, drainTargetType, drainTargetCre
|
|
|
30
30
|
if (keyExist(drainTargetConfig)) {
|
|
31
31
|
body.APIKey = drainTargetConfig.apiKey;
|
|
32
32
|
}
|
|
33
|
+
if (indexPrefixExist(drainTargetConfig)) {
|
|
34
|
+
body.indexPrefix = drainTargetConfig.indexPrefix;
|
|
35
|
+
}
|
|
33
36
|
return body;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
function authorizeDrainCreation (drainTargetType, drainTargetCredentials, drainTargetConfig) {
|
|
37
40
|
if (drainTypeExists(drainTargetType)) {
|
|
38
|
-
// retrieve
|
|
39
|
-
const credStatus =
|
|
40
|
-
const keyStatus =
|
|
41
|
+
// retrieve field for drain type ('mandatory', 'optional', undefined)
|
|
42
|
+
const credStatus = fieldStatus(drainTargetType).credentials;
|
|
43
|
+
const keyStatus = fieldStatus(drainTargetType).apiKey;
|
|
44
|
+
const indexPrefixStatus = fieldStatus(drainTargetType).indexPrefix;
|
|
41
45
|
|
|
42
46
|
if (credStatus === 'MANDATORY') {
|
|
43
47
|
return credentialsExist(drainTargetCredentials);
|
|
@@ -58,10 +62,20 @@ function authorizeDrainCreation (drainTargetType, drainTargetCredentials, drainT
|
|
|
58
62
|
if (!keyStatus) {
|
|
59
63
|
return keyEmpty(drainTargetConfig);
|
|
60
64
|
}
|
|
65
|
+
|
|
66
|
+
if (indexPrefixStatus === 'MANDATORY') {
|
|
67
|
+
return indexPrefixExist(drainTargetConfig);
|
|
68
|
+
}
|
|
69
|
+
if (indexPrefixStatus === 'OPTIONAL') {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (!indexPrefixStatus) {
|
|
73
|
+
return indexPrefixEmpty(drainTargetConfig);
|
|
74
|
+
}
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
77
|
|
|
64
|
-
function
|
|
78
|
+
function fieldStatus (drainTargetType) {
|
|
65
79
|
return DRAIN_TYPES.find(({ id }) => id === drainTargetType);
|
|
66
80
|
}
|
|
67
81
|
|
|
@@ -77,6 +91,14 @@ function credentialsEmpty ({ username, password }) {
|
|
|
77
91
|
return username == null && password == null;
|
|
78
92
|
}
|
|
79
93
|
|
|
94
|
+
function indexPrefixExist ({ indexPrefix }) {
|
|
95
|
+
return indexPrefix != null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function indexPrefixEmpty ({ indexPrefix }) {
|
|
99
|
+
return indexPrefix == null;
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
function keyExist ({ apiKey }) {
|
|
81
103
|
return apiKey != null;
|
|
82
104
|
}
|
package/src/models/log-v4.js
CHANGED
|
@@ -94,7 +94,8 @@ async function watchDeploymentAndDisplayLogs (options) {
|
|
|
94
94
|
redeployDate,
|
|
95
95
|
} = options;
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
// If in quiet mode, we only log start/finished deployment messages
|
|
98
|
+
!quiet && Logger.println('Waiting for deployment to start…');
|
|
98
99
|
const deployment = await waitForDeploymentStart({ ownerId, appId, deploymentId, commitId, knownDeployments });
|
|
99
100
|
Logger.println(colors.bold.blue(`Deployment started (${deployment.uuid})`));
|
|
100
101
|
|
|
@@ -111,7 +112,7 @@ async function watchDeploymentAndDisplayLogs (options) {
|
|
|
111
112
|
logsStream = await displayLogs({ ownerId, appId, deploymentId: deployment.uuid, since: redeployDate, deferred });
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
Logger.println('Waiting for application logs…');
|
|
115
|
+
!quiet && Logger.println('Waiting for application logs…');
|
|
115
116
|
|
|
116
117
|
// Wait for deployment end (or an error thrown by logs with the deferred)
|
|
117
118
|
const deploymentEnded = await Promise.race([
|
package/src/parsers.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const cliparse = require('cliparse');
|
|
4
4
|
|
|
5
5
|
const Application = require('./models/application.js');
|
|
6
|
+
const ISO8601 = require('iso8601-duration');
|
|
7
|
+
const Duration = require('duration-js');
|
|
6
8
|
|
|
7
9
|
function flavor (flavor) {
|
|
8
10
|
const flavors = Application.listAvailableFlavors();
|
|
@@ -32,10 +34,16 @@ function instances (instances) {
|
|
|
32
34
|
|
|
33
35
|
function date (dateString) {
|
|
34
36
|
const date = new Date(dateString);
|
|
35
|
-
if (isNaN(date.getTime())) {
|
|
36
|
-
return cliparse.parsers.
|
|
37
|
+
if (isNaN(dateString) && !isNaN(date.getTime())) {
|
|
38
|
+
return cliparse.parsers.success(date);
|
|
37
39
|
}
|
|
38
|
-
|
|
40
|
+
|
|
41
|
+
const duration = durationInSeconds(dateString);
|
|
42
|
+
if (duration.success) {
|
|
43
|
+
return cliparse.parsers.success(new Date(Date.now() - (duration.success * 1000)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return duration;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
const appIdRegex = /^app_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
@@ -86,6 +94,13 @@ function integer (string) {
|
|
|
86
94
|
return cliparse.parsers.success(integer);
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
function nonEmptyString (string) {
|
|
98
|
+
if (typeof string !== 'string' || string === '') {
|
|
99
|
+
return cliparse.parsers.error('Invalid string, it should not be empty');
|
|
100
|
+
}
|
|
101
|
+
return cliparse.parsers.success(string);
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
// /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/i;
|
|
90
105
|
const tagRegex = /^[^,\s]+$/;
|
|
91
106
|
|
|
@@ -143,6 +158,39 @@ function portNumber (number) {
|
|
|
143
158
|
return cliparse.parsers.error(`Invalid port number '${number}'. Should match ${portNumberRegex}`);
|
|
144
159
|
}
|
|
145
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Parse a duration into seconds
|
|
163
|
+
* A Zero seconds duration is allowed
|
|
164
|
+
* @param {string} durationStr an ISO8601, 1h or a positive number
|
|
165
|
+
* @returns {number} number of seconds
|
|
166
|
+
*/
|
|
167
|
+
function durationInSeconds (durationStr = '') {
|
|
168
|
+
const failed = cliparse.parsers.error(`Invalid duration: "${durationStr}", expect (IS0 8601 duration / a "1h, 1m, 30s" like duration / a positive number in seconds)`);
|
|
169
|
+
|
|
170
|
+
if (durationStr.startsWith('P')) {
|
|
171
|
+
try {
|
|
172
|
+
const d = ISO8601.parse(durationStr);
|
|
173
|
+
return cliparse.parsers.success(ISO8601.toSeconds(d));
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
return failed;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const duration = Duration.parse(durationStr);
|
|
182
|
+
return cliparse.parsers.success(duration.seconds());
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const n = Number.parseInt(durationStr);
|
|
186
|
+
if (isNaN(n) || n < 0) {
|
|
187
|
+
return failed;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return cliparse.parsers.success(n);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
146
194
|
module.exports = {
|
|
147
195
|
buildFlavor,
|
|
148
196
|
flavor,
|
|
@@ -162,4 +210,6 @@ module.exports = {
|
|
|
162
210
|
ipAddress,
|
|
163
211
|
portNumberRegex,
|
|
164
212
|
portNumber,
|
|
213
|
+
durationInSeconds,
|
|
214
|
+
nonEmptyString,
|
|
165
215
|
};
|