clever-tools 3.1.0 → 3.4.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
@@ -10,11 +10,6 @@ Clever Tools are availables from many channels. The simpler way to install them,
10
10
  npm install -g clever-tools
11
11
  ```
12
12
 
13
- If you want to install our latest beta release, you can run:
14
-
15
- ```sh
16
- npm install -g clever-tools@beta
17
- ```
18
13
  We also distribute binaries and packages for multiple systems and tools:
19
14
 
20
15
  * [GNU/Linux](docs/setup-systems.md#gnulinux)
@@ -93,6 +88,7 @@ Where `type` is one of:
93
88
  Where region is one of:
94
89
 
95
90
  - `par` (Paris, [Clever Cloud](https://www.clever-cloud.com/infrastructure/))
91
+ - `grahds` (Gravelines, HDS servers, OVHcloud)
96
92
  - `rbx` (Roubaix, OVHcloud)
97
93
  - `rbxhds` (Roubaix, HDS servers, OVHcloud)
98
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/))
@@ -161,5 +157,5 @@ clever deploy
161
157
 
162
158
  ## Automated releases
163
159
 
164
- This project uses Jenkins to build binaries, package them and release them automatically on the various repositories.
160
+ This project uses GitHub Actions to build binaries, package them and release them automatically on the various repositories.
165
161
  If you want to know more or if you need to release a new version, please read [RELEASE.md](./RELEASE.md) carefully.
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', { description: 'Application 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)',
@@ -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)',
@@ -345,7 +355,7 @@ 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 dependencies' }),
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',
@@ -559,7 +569,7 @@ function run () {
559
569
  const addonCreateCommand = cliparse.command('create', {
560
570
  description: 'Create an addon',
561
571
  args: [args.addonProvider, args.addonName],
562
- options: [opts.linkAddon, opts.confirmAddonCreation, opts.addonPlan, opts.addonRegion, opts.addonVersion, opts.addonOptions],
572
+ options: [opts.linkAddon, opts.confirmAddonCreation, opts.addonPlan, opts.addonRegion, opts.addonVersion, opts.addonOptions, opts.humanJsonOutputFormat],
563
573
  }, addon('create'));
564
574
  const addonDeleteCommand = cliparse.command('delete', {
565
575
  description: 'Delete an addon',
@@ -643,7 +653,7 @@ function run () {
643
653
  const deploy = lazyRequirePromiseModule('../src/commands/deploy.js');
644
654
  const deployCommand = cliparse.command('deploy', {
645
655
  description: 'Deploy an application',
646
- options: [opts.alias, opts.branch, opts.quiet, opts.forceDeploy, opts.followDeployLogs, opts.sameCommitPolicy],
656
+ options: [opts.alias, opts.branch, opts.gitTag, opts.quiet, opts.forceDeploy, opts.followDeployLogs, opts.sameCommitPolicy],
647
657
  }, deploy('deploy'));
648
658
 
649
659
  // DIAG COMMAND
@@ -754,7 +764,7 @@ function run () {
754
764
  const logs = lazyRequirePromiseModule('../src/commands/logs.js');
755
765
  const logsCommand = cliparse.command('logs', {
756
766
  description: 'Fetch application logs, continuously',
757
- options: [opts.alias, opts.before, opts.after, opts.search, opts.deploymentId, opts.addonId],
767
+ options: [opts.alias, opts.before, opts.after, opts.search, opts.deploymentId, opts.addonId, opts.logsFormat],
758
768
  }, logs('appLogs'));
759
769
 
760
770
  // MAKE DEFAULT COMMAND
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clever-tools",
3
- "version": "3.1.0",
3
+ "version": "3.4.0",
4
4
  "description": "Command Line Interface for Clever Cloud.",
5
5
  "main": "bin/clever.js",
6
6
  "keywords": [
@@ -25,14 +25,14 @@
25
25
  "scripts/*.sh"
26
26
  ],
27
27
  "dependencies": {
28
- "@clevercloud/client": "^8.0.2",
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
33
  "curlconverter": "^3.21.0",
34
34
  "eventsource": "^1.1.0",
35
- "isomorphic-git": "^1.25.0",
35
+ "isomorphic-git": "^1.25.3",
36
36
  "linux-release-info": "^3.0.0",
37
37
  "lodash": "^4.17.21",
38
38
  "mkdirp": "^1.0.4",
@@ -34,7 +34,14 @@ async function list (params) {
34
34
 
35
35
  async function create (params) {
36
36
  const [providerName, name] = params.args;
37
- const { link: linkedAppAlias, plan: planName, region, yes: skipConfirmation, org: orgaIdOrName } = params.options;
37
+ const {
38
+ link: linkedAppAlias,
39
+ plan: planName,
40
+ region,
41
+ yes: skipConfirmation,
42
+ org: orgaIdOrName,
43
+ format,
44
+ } = params.options;
38
45
  const version = params.options['addon-version'];
39
46
  const addonOptions = parseAddonOptions(params.options.option);
40
47
 
@@ -42,36 +49,55 @@ async function create (params) {
42
49
  ? await Organisation.getId(orgaIdOrName)
43
50
  : await User.getCurrentId();
44
51
 
52
+ const addonToCreate = {
53
+ ownerId,
54
+ name,
55
+ providerName,
56
+ planName,
57
+ region,
58
+ skipConfirmation,
59
+ version,
60
+ addonOptions,
61
+ };
62
+
45
63
  if (linkedAppAlias != null) {
46
64
  const linkedAppData = await AppConfig.getAppDetails({ alias: linkedAppAlias });
47
- if (orgaIdOrName != null && linkedAppData.ownerId !== ownerId) {
65
+ if (orgaIdOrName != null && linkedAppData.ownerId !== ownerId && format === 'human') {
48
66
  Logger.warn('The specified application does not belong to the specified organisation. Ignoring the `--org` option');
49
67
  }
50
68
  const newAddon = await Addon.create({
69
+ ...addonToCreate,
51
70
  ownerId: linkedAppData.ownerId,
52
- name,
53
- providerName,
54
- planName,
55
- region,
56
- skipConfirmation,
57
- version,
58
- addonOptions,
59
71
  });
60
72
  await Addon.link(linkedAppData.ownerId, linkedAppData.appId, { addon_id: newAddon.id });
61
- Logger.println(`Addon ${name} (id: ${newAddon.id}) successfully created and linked to the application`);
73
+ displayAddon(format, newAddon, `Add-on created and linked to application ${linkedAppAlias} successfully!`);
62
74
  }
63
75
  else {
64
- const newAddon = await Addon.create({
65
- ownerId,
66
- name,
67
- providerName,
68
- planName,
69
- region,
70
- skipConfirmation,
71
- version,
72
- addonOptions,
73
- });
74
- Logger.println(`Addon ${name} (id: ${newAddon.id}) successfully created`);
76
+ const newAddon = await Addon.create(addonToCreate);
77
+ displayAddon(format, newAddon, 'Add-on created successfully!');
78
+ }
79
+ }
80
+
81
+ function displayAddon (format, addon, message) {
82
+ switch (format) {
83
+
84
+ case 'json': {
85
+ Logger.printJson({
86
+ id: addon.id,
87
+ realId: addon.realId,
88
+ name: addon.name,
89
+ });
90
+ break;
91
+ }
92
+
93
+ case 'human':
94
+ default:
95
+ Logger.println([
96
+ message,
97
+ `ID: ${addon.id}`,
98
+ `Real ID: ${addon.realId}`,
99
+ `Name: ${addon.name}`,
100
+ ].join('\n'));
75
101
  }
76
102
  }
77
103
 
@@ -1,17 +1,21 @@
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 [name] = params.args;
10
+ const [rawName] = params.args;
10
11
  const { org: orgaIdOrName, alias, region, github: githubOwnerRepo, format } = 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
+
13
17
  AppConfig.checkAlreadyLinked(apps, name, alias);
14
-
18
+
15
19
  const github = getGithubDetails(githubOwnerRepo);
16
20
  const app = await Application.create(name, typeName, region, orgaIdOrName, github);
17
21
  await AppConfig.addLinkedApplication(app, alias);
@@ -19,13 +23,19 @@ async function create (params) {
19
23
  switch (format) {
20
24
 
21
25
  case 'json': {
22
- console.log(JSON.stringify(app, null, 2));
26
+ Logger.printJson({
27
+ id: app.id,
28
+ name: app.name,
29
+ deployUrl: app.deployUrl,
30
+ });
23
31
  break;
24
32
  }
25
33
 
26
34
  case 'human':
27
35
  default:
28
- Logger.println('Your application has been successfully created!');
36
+ Logger.println('Application created successfully!');
37
+ Logger.println(`ID: ${app.id}`);
38
+ Logger.println(`Name: ${name}`);
29
39
  }
30
40
  };
31
41
 
@@ -36,4 +46,8 @@ function getGithubDetails (githubOwnerRepo) {
36
46
  }
37
47
  }
38
48
 
49
+ function getCurrentDirectoryName () {
50
+ return path.basename(process.cwd());
51
+ }
52
+
39
53
  module.exports = { create };
@@ -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)
@@ -43,7 +43,7 @@ async function deploy (params) {
43
43
  default: {
44
44
  const upToDateMessage = `The clever-cloud application is up-to-date (${colors.green(remoteHeadCommitId)}).\nYou can set a policy with 'same-commit-policy' to handle differently when remote HEAD has the same commit as the one to push.\nOr try this command to restart the application:`;
45
45
  if (commitIdToPush !== deployedCommitId) {
46
- const restartWithId = `clever restart --commit ${commitIdToPush}`
46
+ const restartWithId = `clever restart --commit ${commitIdToPush}`;
47
47
  throw new Error(`${upToDateMessage}\n${colors.yellow(restartWithId)}`);
48
48
  }
49
49
  throw new Error(`${upToDateMessage}\n${colors.yellow('clever restart')}`);
@@ -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
- await git.push(appData.deployUrl, branchRefspec, force)
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 };
@@ -5,26 +5,35 @@ 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');
8
9
 
9
10
  async function appLogs (params) {
10
- const { alias, after: since, before: until, search, 'deployment-id': deploymentId } = params.options;
11
+ const { alias, addon: addonId, after: since, before: until, search, 'deployment-id': deploymentId, format } = params.options;
11
12
 
12
13
  // ignore --search ""
13
14
  const filter = (search !== '') ? search : null;
14
15
 
15
16
  const { appId, ownerId } = await AppConfig.getAppDetails({ alias });
16
17
 
17
- Logger.println('Waiting for application logs…');
18
+ const isForHuman = (format === 'human');
18
19
 
19
20
  // TODO: drop when addons are migrated to the v4 API
20
- if (params.addon) {
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 });
21
+ if (addonId) {
22
+ if (isForHuman) {
23
+ Logger.println(colors.blue('Waiting for addon logs…'));
24
+ }
25
+ else {
26
+ throw new Error(`"${format}" format is not yet available for add-on logs`);
27
+ }
28
+ return LogV2.displayLogs({ appAddonId: addonId, since, until, filter, deploymentId });
29
+ }
30
+
31
+ if (isForHuman) {
32
+ Logger.println(colors.blue('Waiting for application logs…'));
24
33
  }
25
34
 
26
35
  const deferred = new Deferred();
27
- await Log.displayLogs({ ownerId, appId, since, until, filter, deploymentId, deferred });
36
+ await Log.displayLogs({ ownerId, appId, since, until, filter, deploymentId, format, deferred });
28
37
  return deferred.promise;
29
38
  }
30
39
 
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', 'rbx', 'rbxhds', 'scw', 'jed', 'mtl', 'sgp', 'syd', 'wsw'];
20
+ const AVAILABLE_ZONES = ['par', 'grahds', 'rbx', 'rbxhds', 'scw', 'jed', 'mtl', 'sgp', 'syd', 'wsw'];
21
21
 
22
22
  function listAvailableZones () {
23
23
  return autocomplete.words(AVAILABLE_ZONES);
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
- return git.resolveRef({ ...repo, ref: refspec });
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
  };
@@ -1,4 +1,4 @@
1
- const { getHostAndTokens } = require('./send-to-api.js');
1
+ const { getHostAndTokens, processError } = require('./send-to-api.js');
2
2
  const colors = require('colors/safe');
3
3
  const { Deferred } = require('./utils.js');
4
4
  const Logger = require('../logger.js');
@@ -9,17 +9,28 @@ const { ApplicationLogStream } = require('@clevercloud/client/cjs/streams/applic
9
9
  const THROTTLE_ELEMENTS = 2000;
10
10
  const THROTTLE_PER_IN_MILLISECONDS = 100;
11
11
 
12
+ const retryConfiguration = {
13
+ enabled: true,
14
+ maxRetryCount: 6,
15
+ };
16
+
12
17
  async function displayLogs (params) {
13
18
 
14
19
  const deferred = params.deferred || new Deferred();
15
20
  const { apiHost, tokens } = await getHostAndTokens();
16
- 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
+ }
17
26
 
18
27
  const logStream = new ApplicationLogStream({
19
28
  apiHost,
20
29
  tokens,
21
30
  ownerId,
22
31
  appId,
32
+ connectionTimeout: 10_000,
33
+ retryConfiguration,
23
34
  since,
24
35
  until,
25
36
  deploymentId,
@@ -30,21 +41,41 @@ async function displayLogs (params) {
30
41
 
31
42
  // Properly close the stream
32
43
  process.once('SIGINT', (signal) => logStream.close(signal));
44
+ const jsonArray = new JsonArray();
33
45
 
34
46
  logStream
35
47
  .on('open', (event) => {
36
- Logger.debug(`stream opened! ${JSON.stringify({ appId, filter, deploymentId })}`);
48
+ Logger.debug(colors.blue(`Logs stream (open) ${JSON.stringify({ appId, filter, deploymentId })}`));
49
+ if (format === 'json') {
50
+ jsonArray.open();
51
+ }
37
52
  })
38
53
  .on('error', (event) => {
39
- Logger.error(`an error occured: ${event.detail}`);
54
+ Logger.debug(colors.red(`Logs stream (error) ${event.error.message}`));
40
55
  })
41
56
  .onLog((log) => {
42
- Logger.println(formatLogLine(log));
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
+ }
43
68
  });
44
69
 
45
70
  // start() is blocking until end of stream
46
71
  logStream.start()
47
- .then((reason) => deferred.resolve())
72
+ .then((reason) => {
73
+ if (format === 'json') {
74
+ jsonArray.close();
75
+ }
76
+ return deferred.resolve();
77
+ })
78
+ .catch(processError)
48
79
  .catch((error) => deferred.reject(error));
49
80
 
50
81
  return logStream;
@@ -134,3 +165,30 @@ function isBuildSucessMessage (log) {
134
165
  };
135
166
 
136
167
  module.exports = { displayLogs, watchDeploymentAndDisplayLogs };
168
+
169
+ /**
170
+ * Helper to print a real JSON array with starting `[` and ending `]`
171
+ */
172
+ class JsonArray {
173
+ constructor () {
174
+ this._isFirst = true;
175
+ }
176
+
177
+ open () {
178
+ process.stdout.write('[\n');
179
+ }
180
+
181
+ push (log) {
182
+ if (this._isFirst) {
183
+ this._isFirst = false;
184
+ }
185
+ else {
186
+ process.stdout.write(',\n');
187
+ }
188
+ process.stdout.write(` ${JSON.stringify(log)}`);
189
+ }
190
+
191
+ close () {
192
+ process.stdout.write('\n]');
193
+ }
194
+ }
@@ -23,12 +23,22 @@ async function sendToApi (requestParams) {
23
23
  .then(prefixUrl(conf.API_HOST))
24
24
  .then(addOauthHeader(tokens))
25
25
  .then((requestParams) => {
26
- if (process.env.CLEVER_VERBOSE) {
27
- Logger.debug(`${requestParams.method.toUpperCase()} ${requestParams.url} ? ${JSON.stringify(requestParams.queryParams)}`);
28
- }
26
+ Logger.debug(`${requestParams.method.toUpperCase()} ${requestParams.url} ? ${JSON.stringify(requestParams.queryParams)}`);
29
27
  return requestParams;
30
28
  })
31
- .then((requestParams) => request(requestParams, { retry: 1 }));
29
+ .then(request)
30
+ .catch(processError);
31
+ }
32
+
33
+ function processError (error) {
34
+ const code = error.code ?? error?.cause?.code;
35
+ if (code === 'EAI_AGAIN') {
36
+ throw new Error('Cannot reach the Clever Cloud API, please check your internet connection.', { cause: error });
37
+ }
38
+ if (code === 'ECONNRESET') {
39
+ throw new Error('The connection to the Clever Cloud API was closed abruptly, please try again.', { cause: error });
40
+ }
41
+ throw error;
32
42
  }
33
43
 
34
44
  function sendToWarp10 (requestParams) {
@@ -45,4 +55,4 @@ async function getHostAndTokens () {
45
55
  };
46
56
  }
47
57
 
48
- module.exports = { sendToApi, sendToWarp10, getHostAndTokens };
58
+ module.exports = { sendToApi, sendToWarp10, getHostAndTokens, processError };