clever-tools 3.4.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 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: 'dev',
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.4.0",
3
+ "version": "3.5.0",
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": "^3.21.0",
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",
@@ -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
- const formattedAddons = addons.map((addon) => {
25
- return [
26
- addon.plan.name + ' ' + addon.provider.name,
27
- addon.region,
28
- colors.bold.green(addon.name),
29
- addon.id,
30
- ];
31
- });
32
- Logger.println(formatTable(formattedAddons));
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: planName,
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
- Logger.printJson({
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
 
@@ -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
- Logger.println('Application created successfully!');
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
  }
@@ -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 have to add single quotes on values for the parser
21
- const curlString = process.argv
22
- .slice(2)
23
- .map((str) => !str.startsWith('-') ? `'${str}'` : str)
24
- .join(' ');
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
- const curlDetails = parseCurlCommand(curlString);
46
+ if (shouldDisplayCleverCurlHelp) {
47
+ printCleverCurlHelp();
48
+ return;
49
+ }
27
50
 
28
- const tokens = await loadTokens();
51
+ const requestParams = await parseCurlCommand(['curl', ...curlArgs]);
29
52
 
30
- const requestParams = {
31
- method: curlDetails.method,
32
- url: curlDetails.urlWithoutQuery,
33
- headers: curlDetails.headers,
34
- queryParams: curlDetails.query,
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
- const oauthHeader = await Promise.resolve(requestParams)
38
- .then(addOauthHeader(tokens))
39
- .then((request) => request.headers.Authorization);
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
- // Reuse raw curl command
42
- const curlParams = process.argv.slice(3);
71
+ curlArgs.push('-H', `Authorization: ${oauthHeader}`);
72
+ }
43
73
 
44
- // Add oauth
45
- curlParams.push('-H', `Authorization: ${oauthHeader}`);
74
+ spawn('curl', curlArgs, { stdio: 'inherit' });
46
75
 
47
- spawn('curl', curlParams, { stdio: 'inherit' });
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 };
@@ -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 addonId = await resolveRealId(addonIdOrRealId);
18
- const ownerId = await findOwnerId(org, addonId);
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: addonId }).then(sendToApi);
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 formattedLines = backups
28
- .sort((a, b) => a.creation_date.localeCompare(b.creation_date))
29
- .map((backup) => [
30
- backup.backup_id,
31
- backup.creation_date,
32
- backup.status,
33
- ]);
34
-
35
- const head = [
36
- 'BACKUP ID',
37
- 'CREATION DATE',
38
- 'STATUS',
39
- ];
40
-
41
- println(formatTable([
42
- head,
43
- ...formattedLines,
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) {
@@ -25,16 +25,22 @@ async function list (params) {
25
25
  }
26
26
 
27
27
  drains.forEach((drain) => {
28
- const { id, state, target: { url, drainType } } = drain;
29
- Logger.println(`${id} -> ${state} for ${url} as ${drainType}`);
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);
@@ -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: addonId, after: since, before: until, search, 'deployment-id': deploymentId, format } = params.options;
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 (addonId) {
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
  }
@@ -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', 'jed', 'mtl', 'sgp', 'syd', 'wsw'];
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) {
@@ -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 creds for drain type ('mandatory', 'optional', undefined)
39
- const credStatus = credentialsStatus(drainTargetType).credentials;
40
- const keyStatus = credentialsStatus(drainTargetType).apiKey;
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 credentialsStatus (drainTargetType) {
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
  }
@@ -94,7 +94,8 @@ async function watchDeploymentAndDisplayLogs (options) {
94
94
  redeployDate,
95
95
  } = options;
96
96
 
97
- Logger.println('Waiting for deployment to start…');
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.error('Invalid date: ' + dateString + ' (timestamps or IS0 8601 dates are accepted)');
37
+ if (isNaN(dateString) && !isNaN(date.getTime())) {
38
+ return cliparse.parsers.success(date);
37
39
  }
38
- return cliparse.parsers.success(date);
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
  };