clever-tools 2.10.0-beta.2 → 2.10.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,6 +2,10 @@
2
2
 
3
3
  ## Unrelease (????-??-??)
4
4
 
5
+ * add `clever database backups DATABASE-ID` command to list backups of a database
6
+ * add `clever database backups download DATABASE-ID BACKUP-IP` command to download a backup
7
+ * add `clever curl` so we can prefix any `curl` command with `clever` and benefit from the local oAuth v1 auth
8
+
5
9
  ## 2.10.0-beta.2 (2022-09-13)
6
10
 
7
11
  * NetworkGroups fix wrong function call.
package/LICENSE CHANGED
@@ -187,7 +187,7 @@
187
187
  same "printed page" as the copyright notice for easier
188
188
  identification within third-party archives.
189
189
 
190
- Copyright [yyyy] [name of copyright owner]
190
+ Copyright 2022 Clever Cloud
191
191
 
192
192
  Licensed under the Apache License, Version 2.0 (the "License");
193
193
  you may not use this file except in compliance with the License.
package/bin/clever.js CHANGED
@@ -88,6 +88,8 @@ function run () {
88
88
  parser: Parsers.appIdOrName,
89
89
  }),
90
90
  appNameCreation: cliparse.argument('app-name', { description: 'Application name' }),
91
+ backupId: cliparse.argument('backup-id', { description: 'A Database backup ID (format: UUID)' }),
92
+ databaseId: cliparse.argument('database-id', { description: 'A database ID (format: postgres_UUID)' }),
91
93
  drainId: cliparse.argument('drain-id', { description: 'Drain ID' }),
92
94
  drainType: cliparse.argument('drain-type', {
93
95
  description: 'Drain type',
@@ -183,6 +185,10 @@ function run () {
183
185
  metavar: 'commit id',
184
186
  description: 'Restart the application with a specific commit id',
185
187
  }),
188
+ databaseId: cliparse.option('database-id', {
189
+ metavar: 'database_id',
190
+ description: 'The Database ID (postgresql_xxx)',
191
+ }),
186
192
  deploymentId: cliparse.option('deployment-id', {
187
193
  metavar: 'deployment_id',
188
194
  description: 'Fetch logs for a given deployment',
@@ -287,6 +293,10 @@ function run () {
287
293
  description: 'Organisation ID (or name, if unambiguous)',
288
294
  parser: Parsers.orgaIdOrName,
289
295
  }),
296
+ output: cliparse.option('output', {
297
+ aliases: ['out'],
298
+ description: 'redirect the output of the command in a file',
299
+ }),
290
300
  drainPassword: cliparse.option('password', {
291
301
  aliases: ['p'],
292
302
  metavar: 'password',
@@ -996,6 +1006,26 @@ function run () {
996
1006
  commands: [addWebhookCommand, removeWebhookCommand],
997
1007
  }, webhooks('list'));
998
1008
 
1009
+ // DATABASES COMMANDS
1010
+ const database = lazyRequirePromiseModule('../src/commands/database.js');
1011
+ const downloadBackupCommand = cliparse.command('download', {
1012
+ description: 'Download a database backup',
1013
+ args: [args.databaseId, args.backupId],
1014
+ options: [opts.orgaIdOrName, opts.output],
1015
+ }, database('downloadBackups'));
1016
+ const backupsCommand = cliparse.command('backups', {
1017
+ description: 'List available database backups',
1018
+ args: [args.databaseId],
1019
+ options: [opts.orgaIdOrName],
1020
+ commands: [
1021
+ downloadBackupCommand,
1022
+ ],
1023
+ }, database('listBackups'));
1024
+ const databaseCommand = cliparse.command('database', {
1025
+ description: 'List available databases',
1026
+ commands: [backupsCommand],
1027
+ }, () => console.info('not available yet'));
1028
+
999
1029
  // CLI PARSER
1000
1030
  const cliParser = cliparse.cli({
1001
1031
  name: 'clever',
@@ -1012,6 +1042,7 @@ function run () {
1012
1042
  appUnlinkCommand,
1013
1043
  cancelDeployCommand,
1014
1044
  configCommands,
1045
+ databaseCommand,
1015
1046
  deleteCommand,
1016
1047
  deployCommand,
1017
1048
  diagCommand,
@@ -1023,8 +1054,9 @@ function run () {
1023
1054
  logoutCommand,
1024
1055
  logsCommand,
1025
1056
  makeDefaultCommand,
1026
- networkGroupsCommand,
1027
- ngCommand,
1057
+ // Not ready for stable release yet
1058
+ // networkGroupsCommand,
1059
+ // ngCommand,
1028
1060
  openCommand,
1029
1061
  consoleCommand,
1030
1062
  profileCommand,
@@ -1047,4 +1079,11 @@ function run () {
1047
1079
  cliparse.parse(cliParser, cliArgs);
1048
1080
  }
1049
1081
 
1050
- run();
1082
+ // Right now, this is the only way to do this properly
1083
+ // cliparse doesn't allow unknown options/arguments
1084
+ if (process.argv[2] === 'curl') {
1085
+ require('../src/commands/curl.js').curl();
1086
+ }
1087
+ else {
1088
+ run();
1089
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clever-tools",
3
- "version": "2.10.0-beta.2",
3
+ "version": "2.10.0",
4
4
  "description": "Command Line Interface for Clever Cloud.",
5
5
  "main": "bin/clever.js",
6
6
  "keywords": [
@@ -30,6 +30,7 @@
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
34
  "eventsource": "^1.1.0",
34
35
  "isomorphic-git": "^1.8.2",
35
36
  "linux-release-info": "^3.0.0",
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const { parseCurlCommand } = require('curlconverter/util.js');
4
+ const { spawn } = require('child_process');
5
+ const { loadOAuthConf, conf } = require('../models/configuration.js');
6
+ const { addOauthHeader } = require('@clevercloud/client/cjs/oauth.node.js');
7
+
8
+ async function loadTokens () {
9
+ const tokens = await loadOAuthConf();
10
+ return {
11
+ OAUTH_CONSUMER_KEY: conf.OAUTH_CONSUMER_KEY,
12
+ OAUTH_CONSUMER_SECRET: conf.OAUTH_CONSUMER_SECRET,
13
+ API_OAUTH_TOKEN: tokens.token,
14
+ API_OAUTH_TOKEN_SECRET: tokens.secret,
15
+ };
16
+ }
17
+
18
+ async function curl () {
19
+
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(' ');
25
+
26
+ const curlDetails = parseCurlCommand(curlString);
27
+
28
+ const tokens = await loadTokens();
29
+
30
+ const requestParams = {
31
+ method: curlDetails.method,
32
+ url: curlDetails.urlWithoutQuery,
33
+ headers: curlDetails.headers,
34
+ queryParams: curlDetails.query,
35
+ };
36
+
37
+ const oauthHeader = await Promise.resolve(requestParams)
38
+ .then(addOauthHeader(tokens))
39
+ .then((request) => request.headers.Authorization);
40
+
41
+ // Reuse raw curl command
42
+ const curlParams = process.argv.slice(3);
43
+
44
+ // Add oauth
45
+ curlParams.push('-H', `Authorization: ${oauthHeader}`);
46
+
47
+ spawn('curl', curlParams, { stdio: 'inherit' });
48
+
49
+ }
50
+
51
+ module.exports = { curl };
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const { sendToApi } = require('../models/send-to-api.js');
4
+ const { getBackups } = require('@clevercloud/client/cjs/api/v2/backups.js');
5
+ const { println } = require('../logger.js');
6
+ const formatTable = require('../format-table')();
7
+ const superagent = require('superagent');
8
+ const { getSummary } = require('@clevercloud/client/cjs/api/v2/user.js');
9
+ const fs = require('fs');
10
+
11
+ async function listBackups (params) {
12
+
13
+ const [databaseId] = params.args;
14
+ const { org } = params.options;
15
+
16
+ const ownerId = await resolveOwnerId(org, databaseId);
17
+ if (ownerId == null) {
18
+ throw new Error('organisation ID is mandatory');
19
+ }
20
+
21
+ const backups = await getBackups({ ownerId, ref: databaseId }).then(sendToApi);
22
+
23
+ const formattedLines = backups
24
+ .sort((a, b) => a.creation_date.localeCompare(b.creation_date))
25
+ .map((backup) => [
26
+ backup.backup_id,
27
+ backup.creation_date,
28
+ backup.status,
29
+ ]);
30
+
31
+ const head = [
32
+ 'BACKUP ID',
33
+ 'CREATION DATE',
34
+ 'STATUS',
35
+ ];
36
+
37
+ println(formatTable([
38
+ head,
39
+ ...formattedLines,
40
+ ]));
41
+ }
42
+
43
+ async function downloadBackups (params) {
44
+
45
+ const [databaseId, backupId] = params.args;
46
+ const { org, output } = params.options;
47
+
48
+ const ownerId = await resolveOwnerId(org, databaseId);
49
+ if (ownerId == null) {
50
+ throw new Error('organisation ID is mandatory');
51
+ }
52
+
53
+ const backups = await getBackups({ ownerId, ref: databaseId }).then(sendToApi);
54
+ const backup = backups.find((backup) => backup.backup_id === backupId);
55
+
56
+ if (backup == null) {
57
+ throw new Error('no backup with this ID');
58
+ }
59
+
60
+ const res = await superagent
61
+ .get(backup.download_url)
62
+ .responseType('blob');
63
+
64
+ if (output) {
65
+ fs.writeFileSync(output, res.body);
66
+ return;
67
+ }
68
+
69
+ process.stdout.write(res.body);
70
+ }
71
+
72
+ /**
73
+ * Try to get an ownerId from the API
74
+ * @param {*} org cliparse param's option for orga
75
+ * @param {String} databaseId the resource which belong to the owner we are looking for
76
+ * @returns the owner ID (or null if cannot be found)
77
+ */
78
+ async function resolveOwnerId (org, databaseId) {
79
+
80
+ if (org != null && org.orga_name != null) {
81
+ return org.orga_name;
82
+ }
83
+
84
+ const summary = await getSummary().then(sendToApi);
85
+
86
+ const userHasAddon = summary.user.addons.some((addon) => addon.readId === databaseId);
87
+ if (userHasAddon) {
88
+ return summary.user.id;
89
+ }
90
+
91
+ for (const orga of summary.organisations) {
92
+ const orgaHasAddon = orga.addons.some((addon) => addon.realId === databaseId);
93
+ if (orgaHasAddon) {
94
+ return orga.id;
95
+ }
96
+ }
97
+
98
+ return null;
99
+ }
100
+
101
+ module.exports = { listBackups, downloadBackups };
@@ -9,7 +9,7 @@ const NetworkGroup = require('../../models/networkgroup.js');
9
9
  const Formatter = require('../../models/format-string.js');
10
10
  const TableFormatter = require('../../models/format-ng-table.js');
11
11
 
12
- async function listNetworkGroups(params) {
12
+ async function listNetworkGroups (params) {
13
13
  const { org: orgaIdOrName, alias, json } = params.options;
14
14
  const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
15
15
 
@@ -32,7 +32,7 @@ async function listNetworkGroups(params) {
32
32
  }
33
33
  }
34
34
 
35
- async function createNg(params) {
35
+ async function createNg (params) {
36
36
  const { org: orgaIdOrName, alias, label, description, tags, json } = params.options;
37
37
  const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
38
38
 
@@ -49,7 +49,7 @@ async function createNg(params) {
49
49
  }
50
50
  }
51
51
 
52
- async function deleteNg(params) {
52
+ async function deleteNg (params) {
53
53
  const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel } = params.options;
54
54
  const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
55
55
  const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
@@ -9,7 +9,7 @@ const NetworkGroup = require('../../models/networkgroup.js');
9
9
  const Formatter = require('../../models/format-string.js');
10
10
  const TableFormatter = require('../../models/format-ng-table.js');
11
11
 
12
- async function listMembers(params) {
12
+ async function listMembers (params) {
13
13
  const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'natural-name': naturalName, json } = params.options;
14
14
  const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
15
15
  const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
@@ -33,10 +33,10 @@ async function listMembers(params) {
33
33
  }
34
34
  }
35
35
 
36
- async function getMember(params) {
36
+ async function getMember (params) {
37
37
  const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'member-id': memberId, 'natural-name': naturalName, json } = params.options;
38
38
  const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
39
- const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);;
39
+ const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel); ;
40
40
 
41
41
  Logger.info(`Getting details for member ${Formatter.formatString(memberId)} in Network Group ${Formatter.formatString(networkGroupId)}`);
42
42
  const result = await ngApi.getNetworkGroupMember({ ownerId, networkGroupId, memberId: memberId }).then(sendToApi);
@@ -50,7 +50,7 @@ async function getMember(params) {
50
50
  }
51
51
  }
52
52
 
53
- async function addMember(params) {
53
+ async function addMember (params) {
54
54
  const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'member-id': memberId, type, 'domain-name': domainName, label } = params.options;
55
55
  const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
56
56
  const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
@@ -62,7 +62,7 @@ async function addMember(params) {
62
62
  Logger.println(`Successfully added member ${Formatter.formatString(memberId)} to Network Group ${Formatter.formatString(networkGroupId)}.`);
63
63
  }
64
64
 
65
- async function removeMember(params) {
65
+ async function removeMember (params) {
66
66
  const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'member-id': memberId } = params.options;
67
67
  const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
68
68
  const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
@@ -9,7 +9,7 @@ const NetworkGroup = require('../../models/networkgroup.js');
9
9
  const Formatter = require('../../models/format-string.js');
10
10
  const TableFormatter = require('../../models/format-ng-table.js');
11
11
 
12
- async function listPeers(params) {
12
+ async function listPeers (params) {
13
13
  const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, json } = params.options;
14
14
  const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
15
15
  const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
@@ -33,7 +33,7 @@ async function listPeers(params) {
33
33
  }
34
34
  }
35
35
 
36
- async function getPeer(params) {
36
+ async function getPeer (params) {
37
37
  const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'peer-id': peerId, json } = params.options;
38
38
  const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
39
39
  const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
@@ -50,7 +50,7 @@ async function getPeer(params) {
50
50
  }
51
51
  }
52
52
 
53
- async function addExternalPeer(params) {
53
+ async function addExternalPeer (params) {
54
54
  const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, role, 'public-key': publicKey, label, parent, ip, port } = params.options;
55
55
  const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
56
56
  const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
@@ -64,7 +64,7 @@ async function addExternalPeer(params) {
64
64
  return peerId;
65
65
  }
66
66
 
67
- async function removeExternalPeer(params) {
67
+ async function removeExternalPeer (params) {
68
68
  const { org: orgaIdOrName, alias, ng: networkGroupIdOrLabel, 'peer-id': peerId } = params.options;
69
69
  const ownerId = await NetworkGroup.getOwnerId(orgaIdOrName, alias);
70
70
  const networkGroupId = await NetworkGroup.getId(ownerId, networkGroupIdOrLabel);
@@ -89,6 +89,7 @@ const conf = env.getOrElseAll({
89
89
  LOG_HTTP_URL: 'https://api.clever-cloud.com/v2/logs/<%- appId %>',
90
90
  EVENT_URL: 'wss://api.clever-cloud.com/v2/events/event-socket',
91
91
  WARP_10_EXEC_URL: 'https://c1-warp10-clevercloud-customers.services.clever-cloud.com/api/v0/exec',
92
+ // the disclosure of these tokens is not considered as a vulnerability. Do not report this to our security service.
92
93
  OAUTH_CONSUMER_KEY: 'T5nFjKeHH4AIlEveuGhB5S3xg8T19e',
93
94
  OAUTH_CONSUMER_SECRET: 'MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT',
94
95
  SSH_GATEWAY: 'ssh@sshgateway-clevercloud-customers.services.clever-cloud.com',
@@ -7,19 +7,21 @@ const AppConfig = require('./app_configuration.js');
7
7
  const ngApi = require('@clevercloud/client/cjs/api/v4/network-group.js');
8
8
  const { sendToApi } = require('./send-to-api.js');
9
9
 
10
- async function getOwnerId(orgaIdOrName, alias) {
10
+ async function getOwnerId (orgaIdOrName, alias) {
11
11
  if (orgaIdOrName == null) {
12
12
  try {
13
- return (await AppConfig.getAppDetails({alias})).ownerId;
14
- } catch (error) {
15
- return (await User.getCurrentId())
13
+ return (await AppConfig.getAppDetails({ alias })).ownerId;
16
14
  }
17
- } else {
15
+ catch (error) {
16
+ return (await User.getCurrentId());
17
+ }
18
+ }
19
+ else {
18
20
  return (await Organisation.getId(orgaIdOrName));
19
21
  }
20
22
  }
21
23
 
22
- async function getId(ownerId, ngIdOrLabel) {
24
+ async function getId (ownerId, ngIdOrLabel) {
23
25
  if (ngIdOrLabel == null) {
24
26
  return null;
25
27
  }
@@ -32,7 +34,7 @@ async function getId(ownerId, ngIdOrLabel) {
32
34
  .then((ng) => ng.id);
33
35
  }
34
36
 
35
- async function getByLabel(owner_id, label) {
37
+ async function getByLabel (owner_id, label) {
36
38
  const networkGroups = await ngApi.listNetworkGroups({ owner_id }).then(sendToApi);
37
39
  const filteredNgs = networkGroups.filter((ng) => ng.label === label);
38
40
 
@@ -46,11 +48,11 @@ async function getByLabel(owner_id, label) {
46
48
  return filteredNgs[0];
47
49
  }
48
50
 
49
- function listAvailablePeerRoles() {
51
+ function listAvailablePeerRoles () {
50
52
  return autocomplete.words(['client', 'server']);
51
53
  }
52
54
 
53
- function listAvailableMemberTypes() {
55
+ function listAvailableMemberTypes () {
54
56
  return autocomplete.words(['application', 'addon', 'external']);
55
57
  }
56
58