clever-tools 2.10.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,10 +2,14 @@
2
2
 
3
3
  ## Unrelease (????-??-??)
4
4
 
5
+ * skip preorder step on addon creation
6
+ * add `clever addon env` command
7
+ * improve implicit ID params (owner and add-on ID / real ID) for `clever database`
8
+
5
9
  ## 2.10.1 (2023-02-20)
6
10
 
7
11
  * `clever database` command
8
- * Improve output when trying
12
+ * Improve output when trying
9
13
  * `clever database backups` command
10
14
  * Improve output when there are no backups yet
11
15
  * Fix command with personal orga
@@ -13,8 +17,8 @@
13
17
 
14
18
  ## 2.10.0 (2023-02-16)
15
19
 
16
- * add `clever database backups DATABASE-ID` command to list backups of a database
17
- * add `clever database backups download DATABASE-ID BACKUP-IP` command to download a backup
20
+ * add `clever database backups DATABASE-ID` command to list backups of a database
21
+ * add `clever database backups download DATABASE-ID BACKUP-IP` command to download a backup
18
22
  * add `clever curl` so we can prefix any `curl` command with `clever` and benefit from the local oAuth v1 auth
19
23
 
20
24
  ## 2.10.0-beta.2 (2022-09-13)
@@ -55,7 +59,7 @@
55
59
 
56
60
  * Fix linux-release-info usage in `clever diag`
57
61
 
58
- ## 2.7.0 (2020-08-20)
62
+ ## 2.7.0 (2020-08-20)
59
63
 
60
64
  * Improve `clever deploy` perfs in several cases (partial push, force push...)
61
65
  * NOTE: Pushing a brand new repo is still slow in some situations
@@ -64,8 +68,8 @@
64
68
  * `clever domain favourite` just displays the favourite domain (no prefix)
65
69
  * `clever domain favourite set example.com` sets the favourite domain to `example.com`
66
70
  * `clever domain favourite unset` unsets the favourite domain for this app
67
- * Add a message while waiting for deploy to start `clever deploy` and `clever restart`
68
- * Add shallow detection with appropriate error message for `clever deploy`
71
+ * Add a message while waiting for deploy to start `clever deploy` and `clever restart`
72
+ * Add shallow detection with appropriate error message for `clever deploy`
69
73
  * Fix `clever status` typo in output (Clément Delafargue)
70
74
  * Fix `clever env` error message when JSON input is incorrect (Jean Baptiste Noblot)
71
75
  * Update dependencies
@@ -86,7 +90,7 @@ This is the one with the latest isomorphic-git!!!
86
90
  * Add `clever config` command to configure application options
87
91
  * Existing supported options: `name`, `description`, `zero-downtime`, `sticky-sessions` and `cancel-on-push`
88
92
  * New supported option: `force-https`
89
- * Add `--follow` to `clever deploy` and `clever restart` so you can continue to follow logs after the deployment is finished
93
+ * Add `--follow` to `clever deploy` and `clever restart` so you can continue to follow logs after the deployment is finished
90
94
  * Fix #318 where `clever deploy` and `clever restart` would sometimes never exit
91
95
  * We changed the way we detect when a deployment is finished
92
96
  * Fix #304 where `clever logs` would run endlessly even when using `--before/--until`
@@ -94,7 +98,7 @@ This is the one with the latest isomorphic-git!!!
94
98
  ### Internals
95
99
 
96
100
  * We completely removed our dependency to Bacon.js, goodbye old friend 😘
97
- * `@clevercloud/client` was updated to `6.0.0` so we can use the new event based API for logs and events
101
+ * `@clevercloud/client` was updated to `6.0.0` so we can use the new event based API for logs and events
98
102
 
99
103
  ## 2.4.0 (2020-04-16)
100
104
 
@@ -249,7 +253,7 @@ These release were only created for some tests on our CI pipeline.
249
253
 
250
254
  - Log error trace in --verbose mode
251
255
  - Update deps (rename opn => open)
252
- - Fix logs with `clever deploy` (update @clevercloud/client to 2.0.0-beta.1)
256
+ - Fix logs with `clever deploy` (update @clevercloud/client to 2.0.0-beta.1)
253
257
  - Fix typo "connexion" => "connection" in logs and README
254
258
 
255
259
  ## 1.5.0-beta.5 (2019-08-01)
@@ -279,7 +283,7 @@ These release were only created for some tests on our CI pipeline.
279
283
  - Ease testing for preprod with comments in config
280
284
  - Update @clevercloud/client to 2.0.0-beta.0
281
285
 
282
- ## 1.5.0-beta.4 (2019-07-25)
286
+ ## 1.5.0-beta.4 (2019-07-25)
283
287
 
284
288
  - Fix --add-export option for `clever env`
285
289
 
@@ -381,7 +385,7 @@ These release were only created for some tests on our CI pipeline.
381
385
 
382
386
  - Replace [nodegit](https://github.com/nodegit/nodegit) with [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git)
383
387
  - Add ESLint config with a big refactoring to go along
384
- - Always use the npm `colors` package in safe mode: no global `String` pollution
388
+ - Always use the npm `colors` package in safe mode: no global `String` pollution
385
389
  - Build, package and publish with a multibranch pipeline project on Jenkins
386
390
 
387
391
  ### Packaging & distribution
package/README.md CHANGED
@@ -39,7 +39,7 @@ If you are using a GNU/Linux distribution that uses `.deb` packages like Debian
39
39
 
40
40
  ```sh
41
41
  curl -fsSL https://clever-tools.clever-cloud.com/gpg/cc-nexus-deb.public.gpg.key | gpg --dearmor -o /usr/share/keyrings/cc-nexus-deb.gpg
42
- echo "deb [signed-by=/usr/share/keyrings/cc-nexus-deb.gpg] https://nexus.clever-cloud.com/repository/deb stable main" | tee -a /etc/apt/sources.list
42
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/cc-nexus-deb.gpg] https://nexus.clever-cloud.com/repository/deb stable main" | tee -a /etc/apt/sources.list
43
43
  apt-get update
44
44
  apt-get install clever-tools
45
45
  ```
@@ -51,7 +51,7 @@ NOTES:
51
51
  * If you want access to the beta channel, you can use this in your `sources.list`:
52
52
 
53
53
  ```sh
54
- echo "deb [signed-by=/usr/share/keyrings/cc-nexus-deb.gpg] https://nexus.clever-cloud.com/repository/deb-beta beta main" | tee -a /etc/apt/sources.list
54
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/cc-nexus-deb.gpg] https://nexus.clever-cloud.com/repository/deb-beta beta main" | tee -a /etc/apt/sources.list
55
55
  ```
56
56
 
57
57
  #### CentOS/Fedora (.rpm)
@@ -252,7 +252,7 @@ Where `type` is one of:
252
252
  - `php`: for PHP applications
253
253
  - `play1`: for Play1 applications
254
254
  - `play2`: for Play2 applications
255
- - `python`: for python27 applications
255
+ - `python`: for python27 and python3 applications
256
256
  - `ruby`: for ruby applications
257
257
  - `rust`: for rust applications
258
258
  - `sbt`: for applications launched with SBT
@@ -334,9 +334,12 @@ ElasticSearch drains use the Elastic bulk API. To match this endpoint, specify `
334
334
 
335
335
  Datadog has two zones, EU and COM. An account on one zone is not available on the other, make sure to target the good EU or COM intake endpoint.
336
336
 
337
- To create a [Datadog](https://docs.datadoghq.com/api/?lang=python#send-logs-over-http) drain, you just need to use:
337
+ To create a [Datadog](https://docs.datadoghq.com/api/?lang=python#send-logs-over-http) drain, you just need to use one of the following command depending on your zone:
338
338
 
339
339
  ```sh
340
+ # EU
341
+ clever drain create DatadogHTTP "https://http-intake.logs.datadoghq.eu/v1/input/<API_KEY>?ddsource=clevercloud&service=<SERVICE>&host=<HOST>"
342
+ # US
340
343
  clever drain create DatadogHTTP "https://http-intake.logs.datadoghq.com/v1/input/<API_KEY>?ddsource=clevercloud&service=<SERVICE>&host=<HOST>"
341
344
  ```
342
345
 
package/bin/clever.js CHANGED
@@ -13,12 +13,15 @@ if (process.argv.includes('--autocomplete-index')) {
13
13
  }
14
14
 
15
15
  const cliparse = require('cliparse');
16
+ const cliparseCommands = require('cliparse/src/command.js');
16
17
  const updateNotifier = require('update-notifier');
18
+ const _sortBy = require('lodash/sortBy.js');
17
19
 
18
20
  const git = require('../src/models/git.js');
19
21
  const Parsers = require('../src/parsers.js');
20
22
  const handleCommandPromise = require('../src/command-promise-handler.js');
21
23
  const Formatter = require('../src/models/format-string.js');
24
+ const { getOutputFormatOption } = require('../src/get-output-format-option.js');
22
25
 
23
26
  // Exit cleanly if the program we pipe to exits abruptly
24
27
  process.stdout.on('error', (error) => {
@@ -63,7 +66,6 @@ function lazyRequire (modulePath) {
63
66
  };
64
67
  }
65
68
 
66
- const AccessLogs = lazyRequire('../src/models/accesslogs.js');
67
69
  const Addon = lazyRequire('../src/models/addon.js');
68
70
  const Application = lazyRequire('../src/models/application.js');
69
71
  const ApplicationConfiguration = lazyRequire('../src/models/application_configuration.js');
@@ -89,7 +91,7 @@ function run () {
89
91
  }),
90
92
  appNameCreation: cliparse.argument('app-name', { description: 'Application name' }),
91
93
  backupId: cliparse.argument('backup-id', { description: 'A Database backup ID (format: UUID)' }),
92
- databaseId: cliparse.argument('database-id', { description: 'A database ID (format: postgresql_UUID, mysql_UUID, ...)' }),
94
+ databaseId: cliparse.argument('database-id', { description: 'Any database ID (format: addon_UUID, postgresql_UUID, mysql_UUID, ...)' }),
93
95
  drainId: cliparse.argument('drain-id', { description: 'Drain ID' }),
94
96
  drainType: cliparse.argument('drain-type', {
95
97
  description: 'Drain type',
@@ -127,16 +129,8 @@ function run () {
127
129
  // OPTIONS
128
130
  const opts = {
129
131
  sourceableEnvVarsList: cliparse.flag('add-export', { description: 'Display sourceable env variables setting' }),
130
- accesslogsFormat: cliparse.option('format', {
131
- aliases: ['F'],
132
- metavar: 'format',
133
- parser: Parsers.accessLogsFormat,
134
- default: 'simple',
135
- description: 'Output format (one of simple, extended, clf or json)',
136
- complete () {
137
- return cliparse.autocomplete.words(AccessLogs('listAvailableFormats')());
138
- },
139
- }),
132
+ accesslogsFormat: getOutputFormatOption(['simple', 'extended', 'clf']),
133
+ addonEnvFormat: getOutputFormatOption(['shell']),
140
134
  accesslogsFollow: cliparse.flag('follow', {
141
135
  aliases: ['f'],
142
136
  description: 'Display access logs continuously (ignores before/until, after/since)',
@@ -581,10 +575,16 @@ function run () {
581
575
  description: 'List available addon providers',
582
576
  commands: [addonShowProviderCommand],
583
577
  }, addon('listProviders'));
578
+ const addonEnvCommand = cliparse.command('env', {
579
+ description: 'List the environment variables for an add-on',
580
+ options: [opts.addonEnvFormat],
581
+ args: [opts.addonId],
582
+ }, addon('env'));
583
+
584
584
  const addonCommands = cliparse.command('addon', {
585
585
  description: 'Manage addons',
586
586
  options: [opts.orgaIdOrName],
587
- commands: [addonCreateCommand, addonDeleteCommand, addonRenameCommand, addonProvidersCommand],
587
+ commands: [addonCreateCommand, addonDeleteCommand, addonRenameCommand, addonProvidersCommand, addonEnvCommand],
588
588
  }, addon('list'));
589
589
 
590
590
  // APPLICATIONS COMMAND
@@ -1011,7 +1011,7 @@ function run () {
1011
1011
  const downloadBackupCommand = cliparse.command('download', {
1012
1012
  description: 'Download a database backup',
1013
1013
  args: [args.databaseId, args.backupId],
1014
- options: [opts.orgaIdOrName, opts.output],
1014
+ options: [opts.output],
1015
1015
  }, database('downloadBackups'));
1016
1016
  const backupsCommand = cliparse.command('backups', {
1017
1017
  description: 'List available database backups',
@@ -1030,51 +1030,55 @@ function run () {
1030
1030
  console.info('clever database backups download');
1031
1031
  });
1032
1032
 
1033
+ const commands = _sortBy([
1034
+ accesslogsCommand,
1035
+ activityCommand,
1036
+ addonCommands,
1037
+ appCreateCommand,
1038
+ applicationsCommand,
1039
+ appLinkCommand,
1040
+ appUnlinkCommand,
1041
+ cancelDeployCommand,
1042
+ configCommands,
1043
+ databaseCommand,
1044
+ deleteCommand,
1045
+ deployCommand,
1046
+ diagCommand,
1047
+ domainCommands,
1048
+ drainCommands,
1049
+ emailNotificationsCommand,
1050
+ envCommands,
1051
+ cliparseCommands.helpCommand,
1052
+ loginCommand,
1053
+ logoutCommand,
1054
+ logsCommand,
1055
+ makeDefaultCommand,
1056
+ // Not ready for stable release yet
1057
+ // networkGroupsCommand,
1058
+ // ngCommand,
1059
+ openCommand,
1060
+ consoleCommand,
1061
+ profileCommand,
1062
+ publishedConfigCommands,
1063
+ restartCommand,
1064
+ scaleCommand,
1065
+ serviceCommands,
1066
+ sshCommand,
1067
+ statusCommand,
1068
+ stopCommand,
1069
+ tcpRedirsCommands,
1070
+ versionCommand,
1071
+ webhooksCommand,
1072
+ ], 'name');
1073
+
1033
1074
  // CLI PARSER
1034
1075
  const cliParser = cliparse.cli({
1035
1076
  name: 'clever',
1036
1077
  description: 'CLI tool to manage Clever Cloud data and products',
1037
1078
  version: pkg.version,
1038
1079
  options: [opts.verbose, opts.noUpdateNotifier],
1039
- commands: [
1040
- accesslogsCommand,
1041
- activityCommand,
1042
- addonCommands,
1043
- appCreateCommand,
1044
- applicationsCommand,
1045
- appLinkCommand,
1046
- appUnlinkCommand,
1047
- cancelDeployCommand,
1048
- configCommands,
1049
- databaseCommand,
1050
- deleteCommand,
1051
- deployCommand,
1052
- diagCommand,
1053
- domainCommands,
1054
- drainCommands,
1055
- emailNotificationsCommand,
1056
- envCommands,
1057
- loginCommand,
1058
- logoutCommand,
1059
- logsCommand,
1060
- makeDefaultCommand,
1061
- // Not ready for stable release yet
1062
- // networkGroupsCommand,
1063
- // ngCommand,
1064
- openCommand,
1065
- consoleCommand,
1066
- profileCommand,
1067
- publishedConfigCommands,
1068
- restartCommand,
1069
- scaleCommand,
1070
- serviceCommands,
1071
- sshCommand,
1072
- statusCommand,
1073
- stopCommand,
1074
- tcpRedirsCommands,
1075
- versionCommand,
1076
- webhooksCommand,
1077
- ],
1080
+ helpCommand: false,
1081
+ commands,
1078
1082
  });
1079
1083
 
1080
1084
  // Make sure argv[0] is always "node"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clever-tools",
3
- "version": "2.10.1",
3
+ "version": "2.11.0",
4
4
  "description": "Command Line Interface for Clever Cloud.",
5
5
  "main": "bin/clever.js",
6
6
  "keywords": [
@@ -9,7 +9,11 @@ const formatTable = require('../format-table')();
9
9
  const Logger = require('../logger.js');
10
10
  const Organisation = require('../models/organisation.js');
11
11
  const User = require('../models/user.js');
12
- const { parseAddonOptions } = require('../models/addon.js');
12
+ const { parseAddonOptions, findOwnerId } = require('../models/addon.js');
13
+ const { getAllEnvVars } = require('@clevercloud/client/cjs/api/v2/addon.js');
14
+ const { sendToApi } = require('../models/send-to-api.js');
15
+ const { toNameEqualsValueString } = require('@clevercloud/client/cjs/utils/env-vars.js');
16
+ const { resolveAddonId } = require('../models/ids-resolver.js');
13
17
 
14
18
  async function list (params) {
15
19
  const { org: orgaIdOrName } = params.options;
@@ -57,7 +61,16 @@ async function create (params) {
57
61
  Logger.println(`Addon ${name} (id: ${newAddon.id}) successfully created and linked to the application`);
58
62
  }
59
63
  else {
60
- const newAddon = await Addon.create({ ownerId, name, providerName, planName, region, skipConfirmation, version, addonOptions });
64
+ const newAddon = await Addon.create({
65
+ ownerId,
66
+ name,
67
+ providerName,
68
+ planName,
69
+ region,
70
+ skipConfirmation,
71
+ version,
72
+ addonOptions,
73
+ });
61
74
  Logger.println(`Addon ${name} (id: ${newAddon.id}) successfully created`);
62
75
  }
63
76
  }
@@ -142,6 +155,36 @@ async function showProvider (params) {
142
155
  });
143
156
  }
144
157
 
158
+ async function env (params) {
159
+
160
+ const { org, format } = params.options;
161
+ const [addonIdOrRealId] = params.args;
162
+
163
+ const addonId = await resolveAddonId(addonIdOrRealId);
164
+ const ownerId = await findOwnerId(org, addonId);
165
+
166
+ const envFromAddon = await getAllEnvVars({ id: ownerId, addonId }).then(sendToApi);
167
+
168
+ switch (format) {
169
+
170
+ case 'json': {
171
+ const envFromAddonJson = Object.fromEntries(
172
+ envFromAddon.map(({ name, value }) => [name, value]),
173
+ );
174
+ Logger.println(JSON.stringify(envFromAddonJson, null, 2));
175
+ break;
176
+ }
177
+
178
+ case 'shell':
179
+ Logger.println(toNameEqualsValueString(envFromAddon, { addExports: true }));
180
+ break;
181
+
182
+ case 'human':
183
+ default:
184
+ Logger.println(toNameEqualsValueString(envFromAddon, { addExports: false }));
185
+ }
186
+ }
187
+
145
188
  module.exports = {
146
189
  list,
147
190
  create,
@@ -149,4 +192,5 @@ module.exports = {
149
192
  rename,
150
193
  listProviders,
151
194
  showProvider,
195
+ env,
152
196
  };
@@ -5,20 +5,19 @@ const { getBackups } = require('@clevercloud/client/cjs/api/v2/backups.js');
5
5
  const { println } = require('../logger.js');
6
6
  const formatTable = require('../format-table')();
7
7
  const superagent = require('superagent');
8
- const { getSummary } = require('@clevercloud/client/cjs/api/v2/user.js');
9
8
  const fs = require('fs');
9
+ const { findOwnerId } = require('../models/addon.js');
10
+ const { resolveRealId } = require('../models/ids-resolver.js');
10
11
 
11
12
  async function listBackups (params) {
12
13
 
13
- const [databaseId] = params.args;
14
14
  const { org } = params.options;
15
+ const [addonIdOrRealId] = params.args;
15
16
 
16
- const ownerId = await resolveOwnerId(org, databaseId);
17
- if (ownerId == null) {
18
- throw new Error('organisation ID is mandatory');
19
- }
17
+ const addonId = await resolveRealId(addonIdOrRealId);
18
+ const ownerId = await findOwnerId(org, addonId);
20
19
 
21
- const backups = await getBackups({ ownerId, ref: databaseId }).then(sendToApi);
20
+ const backups = await getBackups({ ownerId, ref: addonId }).then(sendToApi);
22
21
 
23
22
  if (backups.length === 0) {
24
23
  println('There are no backups yet');
@@ -47,15 +46,13 @@ async function listBackups (params) {
47
46
 
48
47
  async function downloadBackups (params) {
49
48
 
50
- const [databaseId, backupId] = params.args;
51
49
  const { org, output } = params.options;
50
+ const [addonIdOrRealId, backupId] = params.args;
52
51
 
53
- const ownerId = await resolveOwnerId(org, databaseId);
54
- if (ownerId == null) {
55
- throw new Error('organisation ID is mandatory');
56
- }
52
+ const addonId = await resolveRealId(addonIdOrRealId);
53
+ const ownerId = await findOwnerId(org, addonId);
57
54
 
58
- const backups = await getBackups({ ownerId, ref: databaseId }).then(sendToApi);
55
+ const backups = await getBackups({ ownerId, ref: addonId }).then(sendToApi);
59
56
  const backup = backups.find((backup) => backup.backup_id === backupId);
60
57
 
61
58
  if (backup == null) {
@@ -74,33 +71,4 @@ async function downloadBackups (params) {
74
71
  process.stdout.write(res.body);
75
72
  }
76
73
 
77
- /**
78
- * Try to get an ownerId from the API
79
- * @param {*} org cliparse param's option for orga
80
- * @param {String} databaseId the resource which belong to the owner we are looking for
81
- * @returns the owner ID (or null if cannot be found)
82
- */
83
- async function resolveOwnerId (org, databaseId) {
84
-
85
- if (org != null && org.orga_name != null) {
86
- return org.orga_name;
87
- }
88
-
89
- const summary = await getSummary().then(sendToApi);
90
-
91
- const userHasAddon = summary.user.addons.some((addon) => addon.realId === databaseId);
92
- if (userHasAddon) {
93
- return summary.user.id;
94
- }
95
-
96
- for (const orga of summary.organisations) {
97
- const orgaHasAddon = orga.addons.some((addon) => addon.realId === databaseId);
98
- if (orgaHasAddon) {
99
- return orga.id;
100
- }
101
- }
102
-
103
- return null;
104
- }
105
-
106
74
  module.exports = { listBackups, downloadBackups };
@@ -0,0 +1,23 @@
1
+ const cliparse = require('cliparse');
2
+
3
+ function getOutputFormatOption (formats = []) {
4
+ const availableFormats = ['human', 'json', ...formats];
5
+ return cliparse.option('format', {
6
+ aliases: ['F'],
7
+ metavar: 'format',
8
+ parser: (format) => {
9
+ return availableFormats.includes(format)
10
+ ? cliparse.parsers.success(format)
11
+ : cliparse.parsers.error('The output format must be one of ' + availableFormats.join(', '));
12
+ },
13
+ default: 'human',
14
+ description: `Output format (${availableFormats.join(', ')})`,
15
+ complete () {
16
+ return cliparse.autocomplete.words(availableFormats);
17
+ },
18
+ });
19
+ }
20
+
21
+ module.exports = {
22
+ getOutputFormatOption,
23
+ };
@@ -2,12 +2,10 @@
2
2
 
3
3
  const clfDate = require('clf-date');
4
4
 
5
- function listAvailableFormats () {
6
- return ['simple', 'extended', 'clf', 'json'];
7
- }
8
-
9
5
  function getFormatter (format, isAddon) {
10
6
  switch (format.toLowerCase()) {
7
+ // "simple" is the legacy default, "human" is the new one
8
+ case 'human':
11
9
  case 'simple':
12
10
  return isAddon ? formatSimpleAddon : formatSimple;
13
11
  case 'extended':
@@ -53,4 +51,4 @@ function formatCLFAddon (l) {
53
51
  return `${l.ipS} - - [${clfDate(new Date(l.t))}] "- - -" - ${l.bOut}`;
54
52
  }
55
53
 
56
- module.exports = { listAvailableFormats, getFormatter };
54
+ module.exports = { getFormatter };
@@ -2,8 +2,13 @@
2
2
 
3
3
  const application = require('@clevercloud/client/cjs/api/v2/application.js');
4
4
  const autocomplete = require('cliparse').autocomplete;
5
- const colors = require('colors/safe');
6
- const { get: getAddon, getAll: getAllAddons, remove: removeAddon, create: createAddon, preorder: preorderAddon, update: updateAddon } = require('@clevercloud/client/cjs/api/v2/addon.js');
5
+ const {
6
+ get: getAddon,
7
+ getAll: getAllAddons,
8
+ remove: removeAddon,
9
+ create: createAddon,
10
+ update: updateAddon,
11
+ } = require('@clevercloud/client/cjs/api/v2/addon.js');
7
12
  const { getAllAddonProviders } = require('@clevercloud/client/cjs/api/v2/product.js');
8
13
  const { getSummary } = require('@clevercloud/client/cjs/api/v2/user.js');
9
14
  const { getAddonProvider } = require('@clevercloud/client/cjs/api/v4/addon-providers.js');
@@ -11,6 +16,7 @@ const { getAddonProvider } = require('@clevercloud/client/cjs/api/v4/addon-provi
11
16
  const Interact = require('./interact.js');
12
17
  const Logger = require('../logger.js');
13
18
  const { sendToApi } = require('../models/send-to-api.js');
19
+ const { resolveOwnerId } = require('./ids-resolver.js');
14
20
 
15
21
  function listProviders () {
16
22
  return getAllAddonProviders({}).then(sendToApi);
@@ -167,19 +173,6 @@ async function create ({ ownerId, name, providerName, planName, region, skipConf
167
173
  options: createOptions,
168
174
  };
169
175
 
170
- const result = await preorderAddon({ id: ownerId }, addonToCreate).then(sendToApi);
171
-
172
- if (result.totalTTC > 0 && !skipConfirmation) {
173
- result.lines.forEach(({ description, VAT, price }) => Logger.println(`${description}\tVAT: ${VAT}%\tPrice: ${price}€`));
174
- Logger.println(`Total (without taxes): ${result.totalHT}€`);
175
- Logger.println(colors.bold(`Total (with taxes): ${result.totalTTC}€`));
176
-
177
- await Interact.confirm(
178
- `You're about to pay ${result.totalTTC}€, confirm? (yes or no) `,
179
- 'No confirmation, aborting addon creation',
180
- );
181
- }
182
-
183
176
  return createAddon({ id: ownerId }, addonToCreate).then(sendToApi);
184
177
  }
185
178
 
@@ -255,6 +248,20 @@ async function findById (addonId) {
255
248
  throw new Error(`Could not find add-on with ID: ${addonId}`);
256
249
  }
257
250
 
251
+ async function findOwnerId (org, addonId) {
252
+
253
+ if (org != null && org.orga_id != null) {
254
+ return org.orga_id;
255
+ }
256
+
257
+ const ownerId = await resolveOwnerId(addonId);
258
+ if (ownerId != null) {
259
+ return ownerId;
260
+ }
261
+
262
+ throw new Error(`Add-on ${addonId} does not exist`);
263
+ }
264
+
258
265
  function parseAddonOptions (options) {
259
266
  if (options == null) {
260
267
  return {};
@@ -288,6 +295,7 @@ module.exports = {
288
295
  create,
289
296
  delete: deleteAddon,
290
297
  findById,
298
+ findOwnerId,
291
299
  getProvider,
292
300
  getProviderInfos,
293
301
  link,
@@ -10,44 +10,16 @@ const xdg = require('xdg');
10
10
  const Logger = require('../logger.js');
11
11
  const env = commonEnv(Logger);
12
12
 
13
- function getConfigDir () {
14
- if (process.platform === 'win32') {
15
- return path.resolve(process.env.APPDATA, 'clever-cloud');
16
- }
17
- else {
18
- return xdg.basedir.configPath('clever-cloud');
19
- }
20
- }
21
-
22
- function getConfigPath () {
23
- const configDir = getConfigDir();
24
- return path.resolve(configDir, 'clever-tools.json');
25
- }
26
-
27
- async function isFile (path) {
28
- try {
29
- const pathStat = await fs.stat(path);
30
- return pathStat.isFile();
31
- }
32
- catch (e) {
33
- return false;
34
- }
35
- }
36
-
37
- async function maybeMigrateFromLegacyConfigurationPath () {
38
- // This used to be a file
39
- const configDir = getConfigDir();
40
- const legacyConfig = await isFile(configDir);
41
- // If it is still a file, we replace it with a dir and move it inside
42
- if (legacyConfig) {
43
- const tmpConfigFile = `${configDir}.tmp`;
44
- const configFile = getConfigPath();
13
+ const CONFIG_FILES = {
14
+ MAIN: 'clever-tools.json',
15
+ IDS_CACHE: 'ids-cache.json',
16
+ };
45
17
 
46
- // Rename so that we can create the directory
47
- await fs.rename(configDir, tmpConfigFile);
48
- await mkdirp(configDir, { mode: 0o700 });
49
- await fs.rename(tmpConfigFile, configFile);
50
- }
18
+ function getConfigPath (configFile) {
19
+ const configDir = (process.platform === 'win32')
20
+ ? path.resolve(process.env.APPDATA, 'clever-cloud')
21
+ : xdg.basedir.configPath('clever-cloud');
22
+ return path.resolve(configDir, configFile);
51
23
  }
52
24
 
53
25
  async function loadOAuthConf () {
@@ -59,7 +31,6 @@ async function loadOAuthConf () {
59
31
  };
60
32
  }
61
33
  Logger.debug('Load configuration from ' + conf.CONFIGURATION_FILE);
62
- await maybeMigrateFromLegacyConfigurationPath();
63
34
  try {
64
35
  const rawFile = await fs.readFile(conf.CONFIGURATION_FILE);
65
36
  return JSON.parse(rawFile);
@@ -82,6 +53,32 @@ async function writeOAuthConf (oauthData) {
82
53
  }
83
54
  }
84
55
 
56
+ async function loadIdsCache () {
57
+ const cachePath = getConfigPath(CONFIG_FILES.IDS_CACHE);
58
+ try {
59
+ const rawFile = await fs.readFile(cachePath);
60
+ return JSON.parse(rawFile);
61
+ }
62
+ catch (error) {
63
+ Logger.info(`Cannot load IDs cache from ${cachePath}\n${error.message}`);
64
+ return {
65
+ owners: {},
66
+ addons: {},
67
+ };
68
+ }
69
+ }
70
+
71
+ async function writeIdsCache (ids) {
72
+ const cachePath = getConfigPath(CONFIG_FILES.IDS_CACHE);
73
+ const idsJson = JSON.stringify(ids);
74
+ try {
75
+ await fs.writeFile(cachePath, idsJson);
76
+ }
77
+ catch (error) {
78
+ throw new Error(`Cannot write IDs cache to ${cachePath}\n${error.message}`);
79
+ }
80
+ }
81
+
85
82
  const conf = env.getOrElseAll({
86
83
  API_HOST: 'https://api.clever-cloud.com',
87
84
  // API_HOST: 'https://ccapi-preprod.cleverapps.io',
@@ -94,7 +91,7 @@ const conf = env.getOrElseAll({
94
91
  OAUTH_CONSUMER_SECRET: 'MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT',
95
92
  SSH_GATEWAY: 'ssh@sshgateway-clevercloud-customers.services.clever-cloud.com',
96
93
 
97
- CONFIGURATION_FILE: getConfigPath(),
94
+ CONFIGURATION_FILE: getConfigPath(CONFIG_FILES.MAIN),
98
95
  CONSOLE_TOKEN_URL: 'https://console.clever-cloud.com/cli-oauth',
99
96
  // CONSOLE_TOKEN_URL: 'https://next-console.cleverapps.io/cli-oauth',
100
97
 
@@ -102,4 +99,4 @@ const conf = env.getOrElseAll({
102
99
  APP_CONFIGURATION_FILE: path.resolve('.', '.clever.json'),
103
100
  });
104
101
 
105
- module.exports = { conf, loadOAuthConf, writeOAuthConf };
102
+ module.exports = { conf, loadOAuthConf, writeOAuthConf, loadIdsCache, writeIdsCache };
@@ -42,7 +42,7 @@ const membersTableColumnLengths = [
42
42
  48, /* id length */
43
43
  12, /* type length */
44
44
  48, /* label length */
45
- 24, /* domain-name length */
45
+ 110, /* domain-name length */
46
46
  ];
47
47
  const formatMembersTable = formatNgTable(membersTableColumnLengths);
48
48
  async function formatMembersLine (member, showAliases = false) {
@@ -71,23 +71,23 @@ async function printMembersTableHeader (naturalName = false) {
71
71
 
72
72
  const peersTableColumnLengths = [
73
73
  45, /* id length */
74
+ 15, /* ip */
75
+ 36, /* hostname */
74
76
  12, /* type length */
75
77
  14, /* endpoint type length */
76
- 48, /* label length */
77
- 24, /* hostname */
78
- 15, /* ip */
78
+ 36, /* label length */
79
79
  ];
80
80
  const formatPeersTable = formatNgTable(peersTableColumnLengths);
81
- function formatPeersLine (peer) {
82
- const ip = (peer.endpoint.type === 'ServerEndpoint') ? peer.endpoint.ng_term.ip : peer.endpoint.ng_ip;
81
+ function formatPeersLine(peer) {
82
+ const ip = (peer.endpoint.type === 'ServerEndpoint') ? peer.endpoint.ng_term.host : peer.endpoint.ng_ip;
83
83
  return formatPeersTable([
84
84
  [
85
85
  Formatter.formatId(peer.id),
86
+ Formatter.formatIp(ip),
87
+ Formatter.formatString(peer.hostname, false),
86
88
  Formatter.formatString(peer.type, false),
87
89
  Formatter.formatString(peer.endpoint.type, false),
88
90
  Formatter.formatString(peer.label, false),
89
- Formatter.formatString(peer.hostname, false),
90
- Formatter.formatIp(ip),
91
91
  ],
92
92
  ]);
93
93
  };
@@ -95,11 +95,11 @@ function printPeersTableHeader () {
95
95
  Logger.println(colors.bold(formatPeersTable([
96
96
  [
97
97
  'Peer ID',
98
+ 'IP Address',
99
+ 'Hostname',
98
100
  'Peer Type',
99
101
  'Endpoint Type',
100
102
  'Label',
101
- 'Hostname',
102
- 'IP Address',
103
103
  ],
104
104
  ])));
105
105
  printSeparator(peersTableColumnLengths);
@@ -0,0 +1,110 @@
1
+ const { getSummary } = require('@clevercloud/client/cjs/api/v2/user.js');
2
+ const { sendToApi } = require('./send-to-api.js');
3
+ const { loadIdsCache, writeIdsCache } = require('./configuration.js');
4
+
5
+ /*
6
+ This system uses a simplified representation of the summary to expose IDs links:
7
+
8
+ * app ID => owner ID
9
+ * add-on ID => owner ID
10
+ * real add-on ID => owner ID
11
+ * add-on ID => real add-on ID
12
+ * real add-on ID => add-on ID
13
+
14
+ {
15
+ owners: {
16
+ [appid]: [ownerId],
17
+ [addonId]: [ownerId],
18
+ [realId]: [ownerId],
19
+ },
20
+ addons: {
21
+ [addonId]: { realId: [realId], addonId: [addonId] },
22
+ [realId]: { realId: [realId], addonId: [addonId] },
23
+ },
24
+ }
25
+ */
26
+
27
+ async function resolveOwnerId (id) {
28
+ return getIdFromCacheOrSummary((ids) => ids.owners[id]);
29
+ }
30
+
31
+ async function resolveAddonId (id) {
32
+
33
+ const addonId = await getIdFromCacheOrSummary((ids) => {
34
+ return (ids.addons[id] != null) ? ids.addons[id].addonId : null;
35
+ });
36
+
37
+ if (addonId != null) {
38
+ return addonId;
39
+ }
40
+
41
+ throw new Error(`Add-on ${id} does not exist`);
42
+ }
43
+
44
+ async function resolveRealId (id) {
45
+
46
+ const realId = await getIdFromCacheOrSummary((ids) => {
47
+ return (ids.addons[id] != null) ? ids.addons[id].realId : null;
48
+ });
49
+
50
+ if (realId != null) {
51
+ return realId;
52
+ }
53
+
54
+ throw new Error(`Add-on ${id} does not exist foo`);
55
+ }
56
+
57
+ async function getIdFromCacheOrSummary (callback) {
58
+
59
+ const idsFromCache = await loadIdsCache();
60
+ const idFromCache = callback(idsFromCache);
61
+ if (idFromCache != null) {
62
+ return idFromCache;
63
+ }
64
+
65
+ const idsFromSummary = await getIdsFromSummary();
66
+ await writeIdsCache(idsFromSummary);
67
+
68
+ const idFromSummary = callback(idsFromSummary);
69
+ if (idFromSummary != null) {
70
+ return idFromSummary;
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ async function getIdsFromSummary () {
77
+
78
+ const ids = {
79
+ owners: {},
80
+ addons: {},
81
+ };
82
+
83
+ const summary = await getSummary().then(sendToApi);
84
+
85
+ const owners = [
86
+ summary.user,
87
+ ...summary.organisations,
88
+ ];
89
+
90
+ for (const owner of owners) {
91
+ for (const app of owner.applications) {
92
+ ids.owners[app.id] = owner.id;
93
+ }
94
+ for (const addon of owner.addons) {
95
+ ids.owners[addon.id] = owner.id;
96
+ ids.owners[addon.realId] = owner.id;
97
+ const addonIds = { addonId: addon.id, realId: addon.realId };
98
+ ids.addons[addon.id] = addonIds;
99
+ ids.addons[addon.realId] = addonIds;
100
+ }
101
+ }
102
+
103
+ return ids;
104
+ }
105
+
106
+ module.exports = {
107
+ resolveOwnerId,
108
+ resolveAddonId,
109
+ resolveRealId,
110
+ };
package/src/parsers.js CHANGED
@@ -79,14 +79,6 @@ function commaSeparated (string) {
79
79
  return cliparse.parsers.success(string.split(','));
80
80
  }
81
81
 
82
- function accessLogsFormat (format) {
83
- const availableFormats = AccessLogs.listAvailableFormats();
84
- if (availableFormats.includes(format)) {
85
- return cliparse.parsers.success(format);
86
- }
87
- return cliparse.parsers.error('The format must be one of ' + availableFormats.join(', '));
88
- }
89
-
90
82
  function integer (string) {
91
83
  const integer = parseInt(string);
92
84
  if (isNaN(integer)) {
@@ -162,7 +154,6 @@ module.exports = {
162
154
  addonIdOrName,
163
155
  ngIdOrLabel,
164
156
  commaSeparated,
165
- accessLogsFormat,
166
157
  integer,
167
158
  tag,
168
159
  tags,