clever-tools 3.2.0 → 3.5.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 +43 -11
- package/package.json +6 -4
- package/src/commands/addon.js +109 -31
- package/src/commands/create.js +32 -6
- package/src/commands/curl.js +72 -22
- package/src/commands/database.js +46 -24
- package/src/commands/deploy.js +20 -3
- package/src/commands/drain.js +10 -4
- package/src/commands/logs.js +19 -9
- package/src/commands/status.js +2 -1
- package/src/logger.js +5 -0
- package/src/models/application.js +4 -2
- package/src/models/drain.js +27 -5
- package/src/models/git.js +14 -1
- package/src/models/log-v4.js +56 -5
- package/src/parsers.js +53 -3
package/README.md
CHANGED
|
@@ -88,10 +88,10 @@ Where `type` is one of:
|
|
|
88
88
|
Where region is one of:
|
|
89
89
|
|
|
90
90
|
- `par` (Paris, [Clever Cloud](https://www.clever-cloud.com/infrastructure/))
|
|
91
|
+
- `grahds` (Gravelines, HDS servers, OVHcloud)
|
|
91
92
|
- `rbx` (Roubaix, OVHcloud)
|
|
92
93
|
- `rbxhds` (Roubaix, HDS servers, OVHcloud)
|
|
93
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/))
|
|
94
|
-
- `jed` (Jeddah, Oracle Cloud)
|
|
95
95
|
- `mtl` (Montreal, OVHcloud)
|
|
96
96
|
- `sgp` (Singapore, OVHcloud)
|
|
97
97
|
- `syd` (Sydney, OVHcloud)
|
package/bin/clever.js
CHANGED
|
@@ -90,7 +90,10 @@ function run () {
|
|
|
90
90
|
description: 'Application ID (or name, if unambiguous)',
|
|
91
91
|
parser: Parsers.appIdOrName,
|
|
92
92
|
}),
|
|
93
|
-
appNameCreation: cliparse.argument('app-name', {
|
|
93
|
+
appNameCreation: cliparse.argument('app-name', {
|
|
94
|
+
description: 'Application name (optional, current directory name is used if not specified)',
|
|
95
|
+
default: '',
|
|
96
|
+
}),
|
|
94
97
|
backupId: cliparse.argument('backup-id', { description: 'A Database backup ID (format: UUID)' }),
|
|
95
98
|
databaseId: cliparse.argument('database-id', { description: 'Any database ID (format: addon_UUID, postgresql_UUID, mysql_UUID, ...)' }),
|
|
96
99
|
drainId: cliparse.argument('drain-id', { description: 'Drain ID' }),
|
|
@@ -132,6 +135,7 @@ function run () {
|
|
|
132
135
|
sourceableEnvVarsList: cliparse.flag('add-export', { description: 'Display sourceable env variables setting' }),
|
|
133
136
|
accesslogsFormat: getOutputFormatOption(['simple', 'extended', 'clf']),
|
|
134
137
|
addonEnvFormat: getOutputFormatOption(['shell']),
|
|
138
|
+
logsFormat: getOutputFormatOption(['json-stream']),
|
|
135
139
|
accesslogsFollow: cliparse.flag('follow', {
|
|
136
140
|
aliases: ['f'],
|
|
137
141
|
description: 'Display access logs continuously (ignores before/until, after/since)',
|
|
@@ -165,7 +169,7 @@ function run () {
|
|
|
165
169
|
metavar: 'before',
|
|
166
170
|
aliases: ['until'],
|
|
167
171
|
parser: Parsers.date,
|
|
168
|
-
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)',
|
|
169
173
|
}),
|
|
170
174
|
branch: cliparse.option('branch', {
|
|
171
175
|
aliases: ['b'],
|
|
@@ -180,6 +184,12 @@ function run () {
|
|
|
180
184
|
metavar: 'commit id',
|
|
181
185
|
description: 'Restart the application with a specific commit ID',
|
|
182
186
|
}),
|
|
187
|
+
gitTag: cliparse.option('tag', {
|
|
188
|
+
aliases: ['t'],
|
|
189
|
+
default: '',
|
|
190
|
+
metavar: 'tag',
|
|
191
|
+
description: 'Tag to push (none by default)',
|
|
192
|
+
}),
|
|
183
193
|
databaseId: cliparse.option('database-id', {
|
|
184
194
|
metavar: 'database_id',
|
|
185
195
|
description: 'The Database ID (e.g.: postgresql_xxx)',
|
|
@@ -300,7 +310,7 @@ function run () {
|
|
|
300
310
|
}),
|
|
301
311
|
addonPlan: cliparse.option('plan', {
|
|
302
312
|
aliases: ['p'],
|
|
303
|
-
default: '
|
|
313
|
+
default: '',
|
|
304
314
|
metavar: 'plan',
|
|
305
315
|
description: 'Addon plan, depends on the provider',
|
|
306
316
|
complete: Addon('completePlan'),
|
|
@@ -345,11 +355,17 @@ function run () {
|
|
|
345
355
|
parser: Parsers.commaSeparated,
|
|
346
356
|
}),
|
|
347
357
|
showAllActivity: cliparse.flag('show-all', { description: 'Show all activity' }),
|
|
348
|
-
showAll: cliparse.flag('show-all', { description: 'Show all available
|
|
358
|
+
showAll: cliparse.flag('show-all', { description: 'Show all available add-ons and applications' }),
|
|
349
359
|
loginToken: cliparse.option('token', {
|
|
350
360
|
metavar: 'token',
|
|
351
361
|
description: 'Directly give an existing token',
|
|
352
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
|
+
}),
|
|
353
369
|
instanceType: cliparse.option('type', {
|
|
354
370
|
aliases: ['t'],
|
|
355
371
|
required: true,
|
|
@@ -367,6 +383,12 @@ function run () {
|
|
|
367
383
|
metavar: 'api_key',
|
|
368
384
|
description: 'Drain custom key',
|
|
369
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
|
+
}),
|
|
370
392
|
verbose: cliparse.flag('verbose', { aliases: ['v'], description: 'Verbose output' }),
|
|
371
393
|
withoutCache: cliparse.flag('without-cache', { description: 'Restart the application without using cache' }),
|
|
372
394
|
confirmAddonCreation: cliparse.flag('yes', {
|
|
@@ -559,7 +581,7 @@ function run () {
|
|
|
559
581
|
const addonCreateCommand = cliparse.command('create', {
|
|
560
582
|
description: 'Create an addon',
|
|
561
583
|
args: [args.addonProvider, args.addonName],
|
|
562
|
-
options: [opts.linkAddon, opts.confirmAddonCreation, opts.addonPlan, opts.addonRegion, opts.addonVersion, opts.addonOptions],
|
|
584
|
+
options: [opts.linkAddon, opts.confirmAddonCreation, opts.addonPlan, opts.addonRegion, opts.addonVersion, opts.addonOptions, opts.humanJsonOutputFormat],
|
|
563
585
|
}, addon('create'));
|
|
564
586
|
const addonDeleteCommand = cliparse.command('delete', {
|
|
565
587
|
description: 'Delete an addon',
|
|
@@ -586,7 +608,7 @@ function run () {
|
|
|
586
608
|
|
|
587
609
|
const addonCommands = cliparse.command('addon', {
|
|
588
610
|
description: 'Manage addons',
|
|
589
|
-
options: [opts.orgaIdOrName],
|
|
611
|
+
options: [opts.orgaIdOrName, opts.humanJsonOutputFormat],
|
|
590
612
|
commands: [addonCreateCommand, addonDeleteCommand, addonRenameCommand, addonProvidersCommand, addonEnvCommand],
|
|
591
613
|
}, addon('list'));
|
|
592
614
|
|
|
@@ -629,9 +651,15 @@ function run () {
|
|
|
629
651
|
const appCreateCommand = cliparse.command('create', {
|
|
630
652
|
description: 'Create an application',
|
|
631
653
|
args: [args.appNameCreation],
|
|
632
|
-
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],
|
|
633
655
|
}, create('create'));
|
|
634
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
|
+
|
|
635
663
|
// DELETE COMMAND
|
|
636
664
|
const deleteCommandModule = lazyRequirePromiseModule('../src/commands/delete.js');
|
|
637
665
|
const deleteCommand = cliparse.command('delete', {
|
|
@@ -643,7 +671,7 @@ function run () {
|
|
|
643
671
|
const deploy = lazyRequirePromiseModule('../src/commands/deploy.js');
|
|
644
672
|
const deployCommand = cliparse.command('deploy', {
|
|
645
673
|
description: 'Deploy an application',
|
|
646
|
-
options: [opts.alias, opts.branch, opts.quiet, opts.forceDeploy, opts.followDeployLogs, opts.sameCommitPolicy],
|
|
674
|
+
options: [opts.alias, opts.branch, opts.gitTag, opts.quiet, opts.forceDeploy, opts.followDeployLogs, opts.sameCommitPolicy],
|
|
647
675
|
}, deploy('deploy'));
|
|
648
676
|
|
|
649
677
|
// DIAG COMMAND
|
|
@@ -685,7 +713,7 @@ function run () {
|
|
|
685
713
|
const drainCreateCommand = cliparse.command('create', {
|
|
686
714
|
description: 'Create a drain',
|
|
687
715
|
args: [args.drainType, args.drainUrl],
|
|
688
|
-
options: [opts.addonId, opts.drainUsername, opts.drainPassword, opts.drainAPIKey],
|
|
716
|
+
options: [opts.addonId, opts.drainUsername, opts.drainPassword, opts.drainAPIKey, opts.drainIndexPrefix],
|
|
689
717
|
}, drain('create'));
|
|
690
718
|
const drainRemoveCommand = cliparse.command('remove', {
|
|
691
719
|
description: 'Remove a drain',
|
|
@@ -754,7 +782,7 @@ function run () {
|
|
|
754
782
|
const logs = lazyRequirePromiseModule('../src/commands/logs.js');
|
|
755
783
|
const logsCommand = cliparse.command('logs', {
|
|
756
784
|
description: 'Fetch application logs, continuously',
|
|
757
|
-
options: [opts.alias, opts.before, opts.after, opts.search, opts.deploymentId, opts.addonId],
|
|
785
|
+
options: [opts.alias, opts.before, opts.after, opts.search, opts.deploymentId, opts.addonId, opts.logsFormat],
|
|
758
786
|
}, logs('appLogs'));
|
|
759
787
|
|
|
760
788
|
// MAKE DEFAULT COMMAND
|
|
@@ -1021,7 +1049,7 @@ function run () {
|
|
|
1021
1049
|
const backupsCommand = cliparse.command('backups', {
|
|
1022
1050
|
description: 'List available database backups',
|
|
1023
1051
|
args: [args.databaseId],
|
|
1024
|
-
options: [opts.orgaIdOrName],
|
|
1052
|
+
options: [opts.orgaIdOrName, opts.humanJsonOutputFormat],
|
|
1025
1053
|
commands: [
|
|
1026
1054
|
downloadBackupCommand,
|
|
1027
1055
|
],
|
|
@@ -1035,6 +1063,9 @@ function run () {
|
|
|
1035
1063
|
console.info('clever database backups download');
|
|
1036
1064
|
});
|
|
1037
1065
|
|
|
1066
|
+
// Patch help command description
|
|
1067
|
+
cliparseCommands.helpCommand.description = 'Display help about the Clever Cloud CLI';
|
|
1068
|
+
|
|
1038
1069
|
const commands = _sortBy([
|
|
1039
1070
|
accesslogsCommand,
|
|
1040
1071
|
activityCommand,
|
|
@@ -1045,6 +1076,7 @@ function run () {
|
|
|
1045
1076
|
appUnlinkCommand,
|
|
1046
1077
|
cancelDeployCommand,
|
|
1047
1078
|
configCommands,
|
|
1079
|
+
curlCommand,
|
|
1048
1080
|
databaseCommand,
|
|
1049
1081
|
deleteCommand,
|
|
1050
1082
|
deployCommand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clever-tools",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Command Line Interface for Clever Cloud.",
|
|
5
5
|
"main": "bin/clever.js",
|
|
6
6
|
"keywords": [
|
|
@@ -25,14 +25,16 @@
|
|
|
25
25
|
"scripts/*.sh"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@clevercloud/client": "^8.1.
|
|
28
|
+
"@clevercloud/client": "^8.1.2",
|
|
29
29
|
"clf-date": "^0.2.0",
|
|
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",
|
|
35
|
-
"
|
|
36
|
+
"iso8601-duration": "^2.1.2",
|
|
37
|
+
"isomorphic-git": "^1.25.3",
|
|
36
38
|
"linux-release-info": "^3.0.0",
|
|
37
39
|
"lodash": "^4.17.21",
|
|
38
40
|
"mkdirp": "^1.0.4",
|
package/src/commands/addon.js
CHANGED
|
@@ -15,63 +15,141 @@ 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
|
-
const {
|
|
75
|
+
const {
|
|
76
|
+
link: linkedAppAlias,
|
|
77
|
+
plan: inputPlan,
|
|
78
|
+
region,
|
|
79
|
+
yes: skipConfirmation,
|
|
80
|
+
org: orgaIdOrName,
|
|
81
|
+
format,
|
|
82
|
+
} = params.options;
|
|
38
83
|
const version = params.options['addon-version'];
|
|
39
84
|
const addonOptions = parseAddonOptions(params.options.option);
|
|
85
|
+
const planName = getPlan(inputPlan, providerName);
|
|
40
86
|
|
|
41
87
|
const ownerId = (orgaIdOrName != null)
|
|
42
88
|
? await Organisation.getId(orgaIdOrName)
|
|
43
89
|
: await User.getCurrentId();
|
|
44
90
|
|
|
91
|
+
const addonToCreate = {
|
|
92
|
+
ownerId,
|
|
93
|
+
name,
|
|
94
|
+
providerName,
|
|
95
|
+
planName,
|
|
96
|
+
region,
|
|
97
|
+
skipConfirmation,
|
|
98
|
+
version,
|
|
99
|
+
addonOptions,
|
|
100
|
+
};
|
|
101
|
+
|
|
45
102
|
if (linkedAppAlias != null) {
|
|
46
103
|
const linkedAppData = await AppConfig.getAppDetails({ alias: linkedAppAlias });
|
|
47
|
-
if (orgaIdOrName != null && linkedAppData.ownerId !== ownerId) {
|
|
104
|
+
if (orgaIdOrName != null && linkedAppData.ownerId !== ownerId && format === 'human') {
|
|
48
105
|
Logger.warn('The specified application does not belong to the specified organisation. Ignoring the `--org` option');
|
|
49
106
|
}
|
|
50
107
|
const newAddon = await Addon.create({
|
|
108
|
+
...addonToCreate,
|
|
51
109
|
ownerId: linkedAppData.ownerId,
|
|
52
|
-
name,
|
|
53
|
-
providerName,
|
|
54
|
-
planName,
|
|
55
|
-
region,
|
|
56
|
-
skipConfirmation,
|
|
57
|
-
version,
|
|
58
|
-
addonOptions,
|
|
59
110
|
});
|
|
60
111
|
await Addon.link(linkedAppData.ownerId, linkedAppData.appId, { addon_id: newAddon.id });
|
|
61
|
-
|
|
112
|
+
displayAddon(format, newAddon, providerName, `Add-on created and linked to application ${linkedAppAlias} successfully!`);
|
|
62
113
|
}
|
|
63
114
|
else {
|
|
64
|
-
const newAddon = await Addon.create(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
115
|
+
const newAddon = await Addon.create(addonToCreate);
|
|
116
|
+
displayAddon(format, newAddon, providerName, 'Add-on created successfully!');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function displayAddon (format, addon, providerName, message) {
|
|
121
|
+
switch (format) {
|
|
122
|
+
|
|
123
|
+
case 'json': {
|
|
124
|
+
const jsonAddon = {
|
|
125
|
+
id: addon.id,
|
|
126
|
+
realId: addon.realId,
|
|
127
|
+
name: addon.name,
|
|
128
|
+
};
|
|
129
|
+
Logger.printJson((providerName === 'kv')
|
|
130
|
+
? { ...jsonAddon, availability: 'alpha', warning: 'Don\'t store sensitive or production grade data' }
|
|
131
|
+
: jsonAddon);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'human':
|
|
136
|
+
default:
|
|
137
|
+
Logger.println([
|
|
138
|
+
message,
|
|
139
|
+
`ID: ${addon.id}`,
|
|
140
|
+
`Real ID: ${addon.realId}`,
|
|
141
|
+
`Name: ${addon.name}`,
|
|
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
|
+
}
|
|
75
153
|
}
|
|
76
154
|
}
|
|
77
155
|
|
package/src/commands/create.js
CHANGED
|
@@ -1,31 +1,53 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const path = require('path');
|
|
3
4
|
const Application = require('../models/application.js');
|
|
4
5
|
const AppConfig = require('../models/app_configuration.js');
|
|
5
6
|
const Logger = require('../logger.js');
|
|
6
7
|
|
|
7
8
|
async function create (params) {
|
|
8
9
|
const { type: typeName } = params.options;
|
|
9
|
-
const [
|
|
10
|
-
const { org: orgaIdOrName, alias, region, github: githubOwnerRepo, format } = params.options;
|
|
10
|
+
const [rawName] = params.args;
|
|
11
|
+
const { org: orgaIdOrName, alias, region, github: githubOwnerRepo, format, task: taskCommand } = params.options;
|
|
11
12
|
const { apps } = await AppConfig.loadApplicationConf();
|
|
12
13
|
|
|
14
|
+
// Application name is optionnal, use current directory name if not specified (empty string)
|
|
15
|
+
const name = (rawName !== '') ? rawName : getCurrentDirectoryName();
|
|
16
|
+
|
|
17
|
+
const isTask = (taskCommand != null);
|
|
18
|
+
const envVars = isTask
|
|
19
|
+
? { CC_RUN_COMMAND: taskCommand }
|
|
20
|
+
: {};
|
|
21
|
+
|
|
13
22
|
AppConfig.checkAlreadyLinked(apps, name, alias);
|
|
14
23
|
|
|
15
24
|
const github = getGithubDetails(githubOwnerRepo);
|
|
16
|
-
const app = await Application.create(name, typeName, region, orgaIdOrName, github);
|
|
25
|
+
const app = await Application.create(name, typeName, region, orgaIdOrName, github, isTask, envVars);
|
|
17
26
|
await AppConfig.addLinkedApplication(app, alias);
|
|
18
27
|
|
|
19
28
|
switch (format) {
|
|
20
|
-
|
|
21
29
|
case 'json': {
|
|
22
|
-
|
|
30
|
+
Logger.printJson({
|
|
31
|
+
id: app.id,
|
|
32
|
+
name: app.name,
|
|
33
|
+
executedAs: app.instance.lifetime,
|
|
34
|
+
env: app.env,
|
|
35
|
+
deployUrl: app.deployUrl,
|
|
36
|
+
});
|
|
23
37
|
break;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
case 'human':
|
|
27
41
|
default:
|
|
28
|
-
|
|
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
|
+
}
|
|
49
|
+
Logger.println(`ID: ${app.id}`);
|
|
50
|
+
Logger.println(`Name: ${name}`);
|
|
29
51
|
}
|
|
30
52
|
};
|
|
31
53
|
|
|
@@ -36,4 +58,8 @@ function getGithubDetails (githubOwnerRepo) {
|
|
|
36
58
|
}
|
|
37
59
|
}
|
|
38
60
|
|
|
61
|
+
function getCurrentDirectoryName () {
|
|
62
|
+
return path.basename(process.cwd());
|
|
63
|
+
}
|
|
64
|
+
|
|
39
65
|
module.exports = { create };
|
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/deploy.js
CHANGED
|
@@ -13,12 +13,12 @@ const { sendToApi } = require('../models/send-to-api.js');
|
|
|
13
13
|
// Once the API call to redeploy() has been triggered successfully,
|
|
14
14
|
// the rest (waiting for deployment state to evolve and displaying logs) is done with auto retry (resilient to network failures)
|
|
15
15
|
async function deploy (params) {
|
|
16
|
-
const { alias, branch: branchName, quiet, force, follow, 'same-commit-policy': sameCommitPolicy } = params.options;
|
|
16
|
+
const { alias, branch: branchName, tag: tagName, quiet, force, follow, 'same-commit-policy': sameCommitPolicy } = params.options;
|
|
17
17
|
|
|
18
18
|
const appData = await AppConfig.getAppDetails({ alias });
|
|
19
19
|
const { ownerId, appId } = appData;
|
|
20
|
-
const branchRefspec = await git.getFullBranch(branchName);
|
|
21
20
|
|
|
21
|
+
const branchRefspec = await getBranchToDeploy(branchName, tagName);
|
|
22
22
|
const commitIdToPush = await git.getBranchCommit(branchRefspec);
|
|
23
23
|
const remoteHeadCommitId = await git.getRemoteCommit(appData.deployUrl);
|
|
24
24
|
const deployedCommitId = await Application.get(ownerId, appId)
|
|
@@ -66,7 +66,8 @@ async function deploy (params) {
|
|
|
66
66
|
const knownDeployments = await getAllDeployments({ id: ownerId, appId, limit: 5 }).then(sendToApi);
|
|
67
67
|
|
|
68
68
|
Logger.println('Pushing source code to Clever Cloud…');
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
await git.push(appData.deployUrl, commitIdToPush, force)
|
|
70
71
|
.catch(async (e) => {
|
|
71
72
|
const isShallow = await git.isShallow();
|
|
72
73
|
if (isShallow) {
|
|
@@ -86,4 +87,20 @@ async function restartOnSameCommit (ownerId, appId, commitIdToPush, quiet, follo
|
|
|
86
87
|
return Log.watchDeploymentAndDisplayLogs({ ownerId, appId, deploymentId: restart.deploymentId, quiet, follow });
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
async function getBranchToDeploy (branchName, tagName) {
|
|
91
|
+
if (tagName) {
|
|
92
|
+
const useTag = await git.isExistingTag(tagName);
|
|
93
|
+
if (useTag) {
|
|
94
|
+
const tagRefspec = await git.getFullBranch(tagName);
|
|
95
|
+
return tagRefspec;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
throw new Error(`Tag ${tagName} doesn't exist locally`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
return await git.getFullBranch(branchName);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
module.exports = { deploy };
|
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
|
@@ -5,26 +5,36 @@ const LogV2 = require('../models/log.js');
|
|
|
5
5
|
const Log = require('../models/log-v4.js');
|
|
6
6
|
const Logger = require('../logger.js');
|
|
7
7
|
const { Deferred } = require('../models/utils.js');
|
|
8
|
+
const colors = require('colors/safe');
|
|
9
|
+
const { resolveAddonId } = require('../models/ids-resolver.js');
|
|
8
10
|
|
|
9
11
|
async function appLogs (params) {
|
|
10
|
-
const { alias, after: since, before: until, search, 'deployment-id': deploymentId } = params.options;
|
|
12
|
+
const { alias, addon: addonIdOrRealId, after: since, before: until, search, 'deployment-id': deploymentId, format } = params.options;
|
|
11
13
|
|
|
12
14
|
// ignore --search ""
|
|
13
15
|
const filter = (search !== '') ? search : null;
|
|
16
|
+
const isForHuman = (format === 'human');
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
// TODO: drop when addons are migrated to the v4 API
|
|
19
|
+
if (addonIdOrRealId != null) {
|
|
20
|
+
const addonId = await resolveAddonId(addonIdOrRealId);
|
|
21
|
+
if (isForHuman) {
|
|
22
|
+
Logger.println(colors.blue('Waiting for addon logs…'));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
throw new Error(`"${format}" format is not yet available for add-on logs`);
|
|
26
|
+
}
|
|
27
|
+
return LogV2.displayLogs({ appAddonId: addonId, since, until, filter, deploymentId });
|
|
28
|
+
}
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
const { appId, ownerId } = await AppConfig.getAppDetails({ alias });
|
|
18
31
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const { addon: addonId } = params.options;
|
|
22
|
-
const appAddonId = addonId || await AppConfig.getAppDetails({ alias }).then(({ appId }) => appId);
|
|
23
|
-
return LogV2.displayLogs({ appAddonId, since, until, filter, deploymentId });
|
|
32
|
+
if (isForHuman) {
|
|
33
|
+
Logger.println(colors.blue('Waiting for application logs…'));
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
const deferred = new Deferred();
|
|
27
|
-
await Log.displayLogs({ ownerId, appId, since, until, filter, deploymentId, deferred });
|
|
37
|
+
await Log.displayLogs({ ownerId, appId, since, until, filter, deploymentId, format, deferred });
|
|
28
38
|
return deferred.promise;
|
|
29
39
|
}
|
|
30
40
|
|
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) {
|
package/src/logger.js
CHANGED
|
@@ -52,6 +52,11 @@ const Logger = _(['debug', 'info', 'warn', 'error'])
|
|
|
52
52
|
// No decoration for Logger.println
|
|
53
53
|
Logger.println = console.log;
|
|
54
54
|
|
|
55
|
+
// No decoration for Logger.println
|
|
56
|
+
Logger.printJson = (obj) => {
|
|
57
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
58
|
+
};
|
|
59
|
+
|
|
55
60
|
// No decoration for Logger.printErrorLine
|
|
56
61
|
Logger.printErrorLine = console.error;
|
|
57
62
|
|
|
@@ -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', '
|
|
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/git.js
CHANGED
|
@@ -78,7 +78,19 @@ async function getFullBranch (branchName) {
|
|
|
78
78
|
|
|
79
79
|
async function getBranchCommit (refspec) {
|
|
80
80
|
const repo = await getRepo();
|
|
81
|
-
|
|
81
|
+
const oid = await git.resolveRef({ ...repo, ref: refspec });
|
|
82
|
+
// When a refspec refers to an annotated tag, the OID ref represents the annotation and not the commit directly,
|
|
83
|
+
// that's why we need a call to `readCommit`.
|
|
84
|
+
const res = await git.readCommit({ ...repo, ref: refspec, oid });
|
|
85
|
+
return res.oid;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function isExistingTag (tag) {
|
|
89
|
+
const repo = await getRepo();
|
|
90
|
+
const tags = await git.listTags({
|
|
91
|
+
...repo,
|
|
92
|
+
});
|
|
93
|
+
return tags.includes(tag);
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
async function push (remoteUrl, branchRefspec, force) {
|
|
@@ -131,4 +143,5 @@ module.exports = {
|
|
|
131
143
|
push,
|
|
132
144
|
completeBranches,
|
|
133
145
|
isShallow,
|
|
146
|
+
isExistingTag,
|
|
134
147
|
};
|
package/src/models/log-v4.js
CHANGED
|
@@ -18,7 +18,11 @@ async function displayLogs (params) {
|
|
|
18
18
|
|
|
19
19
|
const deferred = params.deferred || new Deferred();
|
|
20
20
|
const { apiHost, tokens } = await getHostAndTokens();
|
|
21
|
-
const { ownerId, appId, filter, since, until, deploymentId } = params;
|
|
21
|
+
const { ownerId, appId, filter, since, until, deploymentId, format } = params;
|
|
22
|
+
|
|
23
|
+
if (format === 'json' && until == null) {
|
|
24
|
+
throw new Error('"json" format is only applicable with a limiting parameter such as `--until`');
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
const logStream = new ApplicationLogStream({
|
|
24
28
|
apiHost,
|
|
@@ -37,21 +41,40 @@ async function displayLogs (params) {
|
|
|
37
41
|
|
|
38
42
|
// Properly close the stream
|
|
39
43
|
process.once('SIGINT', (signal) => logStream.close(signal));
|
|
44
|
+
const jsonArray = new JsonArray();
|
|
40
45
|
|
|
41
46
|
logStream
|
|
42
47
|
.on('open', (event) => {
|
|
43
48
|
Logger.debug(colors.blue(`Logs stream (open) ${JSON.stringify({ appId, filter, deploymentId })}`));
|
|
49
|
+
if (format === 'json') {
|
|
50
|
+
jsonArray.open();
|
|
51
|
+
}
|
|
44
52
|
})
|
|
45
53
|
.on('error', (event) => {
|
|
46
54
|
Logger.debug(colors.red(`Logs stream (error) ${event.error.message}`));
|
|
47
55
|
})
|
|
48
56
|
.onLog((log) => {
|
|
49
|
-
|
|
57
|
+
switch (format) {
|
|
58
|
+
case 'json':
|
|
59
|
+
jsonArray.push(log);
|
|
60
|
+
return;
|
|
61
|
+
case 'json-stream':
|
|
62
|
+
Logger.printJson(log);
|
|
63
|
+
return;
|
|
64
|
+
case 'human':
|
|
65
|
+
default:
|
|
66
|
+
Logger.println(formatLogLine(log));
|
|
67
|
+
}
|
|
50
68
|
});
|
|
51
69
|
|
|
52
70
|
// start() is blocking until end of stream
|
|
53
71
|
logStream.start()
|
|
54
|
-
.then((reason) =>
|
|
72
|
+
.then((reason) => {
|
|
73
|
+
if (format === 'json') {
|
|
74
|
+
jsonArray.close();
|
|
75
|
+
}
|
|
76
|
+
return deferred.resolve();
|
|
77
|
+
})
|
|
55
78
|
.catch(processError)
|
|
56
79
|
.catch((error) => deferred.reject(error));
|
|
57
80
|
|
|
@@ -71,7 +94,8 @@ async function watchDeploymentAndDisplayLogs (options) {
|
|
|
71
94
|
redeployDate,
|
|
72
95
|
} = options;
|
|
73
96
|
|
|
74
|
-
|
|
97
|
+
// If in quiet mode, we only log start/finished deployment messages
|
|
98
|
+
!quiet && Logger.println('Waiting for deployment to start…');
|
|
75
99
|
const deployment = await waitForDeploymentStart({ ownerId, appId, deploymentId, commitId, knownDeployments });
|
|
76
100
|
Logger.println(colors.bold.blue(`Deployment started (${deployment.uuid})`));
|
|
77
101
|
|
|
@@ -88,7 +112,7 @@ async function watchDeploymentAndDisplayLogs (options) {
|
|
|
88
112
|
logsStream = await displayLogs({ ownerId, appId, deploymentId: deployment.uuid, since: redeployDate, deferred });
|
|
89
113
|
}
|
|
90
114
|
|
|
91
|
-
Logger.println('Waiting for application logs…');
|
|
115
|
+
!quiet && Logger.println('Waiting for application logs…');
|
|
92
116
|
|
|
93
117
|
// Wait for deployment end (or an error thrown by logs with the deferred)
|
|
94
118
|
const deploymentEnded = await Promise.race([
|
|
@@ -142,3 +166,30 @@ function isBuildSucessMessage (log) {
|
|
|
142
166
|
};
|
|
143
167
|
|
|
144
168
|
module.exports = { displayLogs, watchDeploymentAndDisplayLogs };
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Helper to print a real JSON array with starting `[` and ending `]`
|
|
172
|
+
*/
|
|
173
|
+
class JsonArray {
|
|
174
|
+
constructor () {
|
|
175
|
+
this._isFirst = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
open () {
|
|
179
|
+
process.stdout.write('[\n');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
push (log) {
|
|
183
|
+
if (this._isFirst) {
|
|
184
|
+
this._isFirst = false;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
process.stdout.write(',\n');
|
|
188
|
+
}
|
|
189
|
+
process.stdout.write(` ${JSON.stringify(log)}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
close () {
|
|
193
|
+
process.stdout.write('\n]');
|
|
194
|
+
}
|
|
195
|
+
}
|
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
|
};
|